tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades.
If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is a python API to work with some methods of Tinkoff Open API using REST protocol. 6It can view history, orders and market information. Also, you can open orders and trades. 7 8If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command. 9**See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 10 11**Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 12 13About Tinkoff Invest API: https://tinkoff.github.io/investAPI/ 14 15Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/ 16""" 17 18# Copyright (c) 2022 Gilmillin Timur Mansurovich 19# 20# Licensed under the Apache License, Version 2.0 (the "License"); 21# you may not use this file except in compliance with the License. 22# You may obtain a copy of the License at 23# 24# http://www.apache.org/licenses/LICENSE-2.0 25# 26# Unless required by applicable law or agreed to in writing, software 27# distributed under the License is distributed on an "AS IS" BASIS, 28# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29# See the License for the specific language governing permissions and 30# limitations under the License. 31 32 33import sys 34import os 35from argparse import ArgumentParser 36from importlib.metadata import version 37 38from datetime import datetime, timedelta 39from dateutil.tz import tzlocal, tzutc 40from time import sleep 41 42import re 43import json 44import requests 45import traceback as tb 46from typing import Union 47 48from multiprocessing import cpu_count 49from multiprocessing.pool import ThreadPool 50import pandas as pd 51 52from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 53 54from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 55from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 56 57import UniLogger as uLog # Logger for TKSBrokerAPI 58 59 60# --- Common technical parameters: 61 62PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 63uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 64uLogger.level = 10 # debug level by default for TKSBrokerAPI module 65uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 66 67__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 68 69CPU_COUNT = cpu_count() # host's real CPU count 70CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 71 72# --- Main constants: 73 74NANO = 0.000000001 # SI-constant nano = 10^-9 75 76 77def NanoToFloat(units: str, nano: int) -> float: 78 """ 79 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 80 81 `NanoToFloat(units="2", nano=500000000) -> 2.5` 82 83 `NanoToFloat(units="0", nano=50000000) -> 0.05` 84 85 :param units: integer string or integer parameter that represents the integer part of number 86 :param nano: integer string or integer parameter that represents the fractional part of number 87 :return: float view of number 88 """ 89 return int(units) + int(nano) * NANO 90 91 92def FloatToNano(number: float) -> dict: 93 """ 94 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 95 96 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 97 98 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 99 100 :param number: float number 101 :return: nano-type view of number: `{"units": "string", "nano": integer}` 102 """ 103 splitByPoint = str(number).split(".") 104 frac = 0 105 106 if len(splitByPoint) > 1: 107 if len(splitByPoint[1]) <= 9: 108 frac = int("{}{}".format( 109 int(splitByPoint[1]), 110 "0" * (9 - len(splitByPoint[1])), 111 )) 112 113 if (number < 0) and (frac > 0): 114 frac = -frac 115 116 return {"units": str(int(number)), "nano": frac} 117 118 119def GetDatesAsString(start: str = None, end: str = None) -> tuple: 120 """ 121 Create tuple of date and time strings with timezone parsed from user-friendly date. 122 123 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 124 125 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 126 An error exception will occur if input date has incorrect format. 127 128 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 129 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 130 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 131 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 132 133 Also, you can use keywords for start if `end=None`: 134 `today` (from 00:00:00 to the end of current day), 135 `yesterday` (-1 day from 00:00:00 to 23:59:59), 136 `week` (-7 day from 00:00:00 to the end of current day), 137 `month` (-30 day from 00:00:00 to the end of current day), 138 `year` (-365 day from 00:00:00 to the end of current day), 139 140 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 141 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 142 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 143 """ 144 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 145 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 146 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 147 148 # time between start and the end of the current day: 149 if start is None or start.lower() == "today": 150 pass 151 152 # from start of the last day to the end of the last day: 153 elif start.lower() == "yesterday": 154 s -= timedelta(days=1) 155 e -= timedelta(days=1) 156 157 # week (-7 day from 00:00:00 to the end of the current day): 158 elif start.lower() == "week": 159 s -= timedelta(days=6) # +1 current day already taken into account 160 161 # month (-30 day from 00:00:00 to the end of current day): 162 elif start.lower() == "month": 163 s -= timedelta(days=29) # +1 current day already taken into account 164 165 # year (-365 day from 00:00:00 to the end of current day): 166 elif start.lower() == "year": 167 s -= timedelta(days=364) # +1 current day already taken into account 168 169 # -N days ago to the end of current day: 170 elif start.startswith('-') and start[1:].isdigit(): 171 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 172 173 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 174 else: 175 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 176 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 177 178 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 179 s = s.strftime(TKS_DATE_TIME_FORMAT) 180 e = e.strftime(TKS_DATE_TIME_FORMAT) 181 182 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 183 184 return s, e 185 186 187class TinkoffBrokerServer: 188 """ 189 This class implements methods to work with Tinkoff broker server. 190 191 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 192 193 About `token`: https://tinkoff.github.io/investAPI/token/ 194 """ 195 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 196 """ 197 Main class init. 198 199 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 200 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 201 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 202 :param useCache: use default cache file with raw data to use instead of `iList`. 203 True by default. Cache is auto-update if new day has come. 204 If you don't want to use cache and always updates raw data then set `useCache=False`. 205 :param defaultCache: path to default cache file. `dump.json` by default. 206 """ 207 if token is None or not token: 208 try: 209 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 210 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 211 212 except KeyError: 213 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 214 raise Exception("Token required") 215 216 else: 217 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 218 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 219 220 if accountId is None or not accountId: 221 try: 222 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 223 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 224 225 except KeyError: 226 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 227 228 else: 229 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 230 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 231 232 self.version = __version__ # duplicate here used TKSBrokerAPI main version 233 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 234 235 Latest version: https://pypi.org/project/tksbrokerapi/ 236 """ 237 238 self.aliases = TKS_TICKER_ALIASES 239 """Some aliases instead official tickers. 240 241 See also: `TKSEnums.TKS_TICKER_ALIASES` 242 """ 243 244 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 245 246 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 247 248 self.ticker = "" 249 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 250 251 See also: `SearchByTicker()`, `SearchInstruments()`. 252 """ 253 254 self.figi = "" 255 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 256 257 See also: `SearchByFIGI()`, `SearchInstruments()`. 258 """ 259 260 self.depth = 1 261 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 262 263 See also: `GetCurrentPrices()`. 264 """ 265 266 self.server = r"https://invest-public-api.tinkoff.ru/rest" 267 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 268 269 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 270 """ 271 272 uLogger.debug("Broker API server: {}".format(self.server)) 273 274 self.timeout = 15 275 """Server operations timeout in seconds. Default: `15`. 276 277 See also: `SendAPIRequest()`. 278 """ 279 280 self.headers = { 281 "Content-Type": "application/json", 282 "accept": "application/json", 283 "Authorization": "Bearer {}".format(self.token), 284 "x-app-name": "Tim55667757.TKSBrokerAPI", 285 } 286 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 287 288 See also: `SendAPIRequest()`. 289 """ 290 291 self.body = None 292 """Request body which send to broker server. Default: `None`. 293 294 See also: `SendAPIRequest()`. 295 """ 296 297 self.historyFile = None 298 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe. 299 300 See also: `History()`. 301 """ 302 303 self.htmlHistoryFile = "index.html" 304 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 305 306 See also: `ShowHistoryChart()`. 307 """ 308 309 self.instrumentsFile = "instruments.md" 310 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 311 312 See also: `ShowInstrumentsInfo()`. 313 """ 314 315 self.searchResultsFile = "search-results.md" 316 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 317 318 See also: `SearchInstruments()`. 319 """ 320 321 self.pricesFile = "prices.md" 322 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 323 324 See also: `GetListOfPrices()`. 325 """ 326 327 self.infoFile = "info.md" 328 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 329 330 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 331 """ 332 333 self.bondsXLSXFile = "ext-bonds.xlsx" 334 """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 335 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 336 337 See also: `ExtendBondsData()`. 338 """ 339 340 self.calendarFile = "calendar.md" 341 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 342 343 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 344 345 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 346 """ 347 348 self.overviewFile = "overview.md" 349 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 350 351 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 352 """ 353 354 self.overviewDigestFile = "overview-digest.md" 355 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 356 357 See also: `Overview()` with parameter `details="digest"`. 358 """ 359 360 self.overviewPositionsFile = "overview-positions.md" 361 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 362 363 See also: `Overview()` with parameter `details="positions"`. 364 """ 365 366 self.overviewOrdersFile = "overview-orders.md" 367 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 368 369 See also: `Overview()` with parameter `details="orders"`. 370 """ 371 372 self.overviewAnalyticsFile = "overview-analytics.md" 373 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 374 375 See also: `Overview()` with parameter `details="analytics"`. 376 """ 377 378 self.reportFile = "deals.md" 379 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 380 381 See also: `Deals()`. 382 """ 383 384 self.withdrawalLimitsFile = "limits.md" 385 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 386 387 See also: `OverviewLimits()` and `RequestLimits()`. 388 """ 389 390 self.userInfoFile = "user-info.md" 391 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 392 393 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 394 """ 395 396 self.userAccountsFile = "accounts.md" 397 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 398 399 See also: `OverviewAccounts()`, `RequestAccounts()`. 400 """ 401 402 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 403 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 404 405 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 406 407 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 408 """ 409 410 self.iList = None # init iList for raw instruments data 411 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 412 413 See also: `Listing()`, `DumpInstruments()`. 414 """ 415 416 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 417 if useCache: 418 if os.path.exists(self.iListDumpFile): 419 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 420 curTime = datetime.now(tzutc()) 421 422 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 423 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 424 425 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 426 427 else: 428 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 429 430 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 431 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 432 433 else: 434 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 435 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 436 437 else: 438 self.iList = self.Listing() # request new raw instruments data from broker server 439 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 440 441 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 442 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 443 444 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 445 """ 446 447 @staticmethod 448 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 449 """ 450 Parse JSON from response string. 451 452 :param rawData: this is a string with JSON-formatted text. 453 :param debug: if `True` then print more debug information. 454 :return: JSON (dictionary), parsed from server response string. 455 """ 456 if debug: 457 uLogger.debug("Raw text body:") 458 uLogger.debug(rawData) 459 460 responseJSON = json.loads(rawData) if rawData else {} 461 462 if debug: 463 uLogger.debug("JSON formatted:") 464 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 465 uLogger.debug(jsonLine) 466 467 return responseJSON 468 469 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 470 """ 471 Send GET or POST request to broker server and receive JSON object. 472 473 self.header: must be defining with dictionary of headers. 474 self.body: if define then used as request body. None by default. 475 self.timeout: global request timeout, 15 seconds by default. 476 :param url: url with REST request. 477 :param reqType: send "GET" or "POST" request. "GET" by default. 478 :param retry: how many times retry after first request if an 5xx server errors occurred. 479 :param pause: sleep time in seconds between retries. 480 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 481 :return: response JSON (dictionary) from broker. 482 """ 483 if reqType not in ("GET", "POST"): 484 uLogger.error("You can define request type: 'GET' or 'POST'!") 485 raise Exception("Incorrect value") 486 487 if debug: 488 uLogger.debug("Request parameters:") 489 uLogger.debug(" - REST API URL: {}".format(url)) 490 uLogger.debug(" - request type: {}".format(reqType)) 491 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 492 uLogger.debug(" - body: {}".format(self.body)) 493 494 # fast hack to avoid all operations with some tickers/FIGI 495 responseJSON = {} 496 oK = True 497 for item in self.exclude: 498 if item in url: 499 if debug: 500 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 501 502 oK = False 503 break 504 505 if oK: 506 counter = 0 507 response = None 508 errMsg = "" 509 510 while not response and counter <= retry: 511 if reqType == "GET": 512 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 513 514 if reqType == "POST": 515 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if debug: 518 uLogger.debug("Response:") 519 uLogger.debug(" - status code: {}".format(response.status_code)) 520 uLogger.debug(" - reason: {}".format(response.reason)) 521 uLogger.debug(" - body length: {}".format(len(response.text))) 522 uLogger.debug(" - headers: {}".format(response.headers)) 523 524 # Server returns some headers: 525 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 526 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 527 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 528 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 529 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 530 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 531 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 532 sleep(rateLimitWait) 533 534 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 535 if 400 <= response.status_code < 500: 536 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 537 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 538 counter = retry + 1 539 540 if 500 <= response.status_code < 600: 541 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 542 uLogger.debug(" - not oK, {}".format(errMsg)) 543 counter += 1 544 545 if counter <= retry: 546 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 547 sleep(pause) 548 549 responseJSON = self._ParseJSON(response.text) 550 551 if errMsg: 552 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 553 uLogger.error(" - not oK, {}".format(errMsg)) 554 555 return responseJSON 556 557 def _IUpdater(self, iType: str) -> tuple: 558 """ 559 Request instrument by type from server. See available API methods for instruments: 560 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 561 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 562 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 563 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 564 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 565 566 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 567 :return: tuple with iType name and list of available instruments of current type for defined user token. 568 """ 569 result = [] 570 571 if iType in TKS_INSTRUMENTS: 572 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 573 574 # all instruments have the same body in API v2 requests: 575 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 576 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 577 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 578 579 return iType, result 580 581 def _IWrapper(self, kwargs): 582 """ 583 Wrapper runs instrument's update method `_IUpdater()`. 584 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 585 """ 586 return self._IUpdater(**kwargs) 587 588 def Listing(self) -> dict: 589 """ 590 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 591 592 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 593 """ 594 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 595 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 596 597 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 598 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 599 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 600 601 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 602 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 603 poolUpdater.close() 604 605 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 606 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 607 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 608 609 # calculate minimum price increment (step) for all instruments and set up instrument's type: 610 for iType in iList.keys(): 611 for ticker in iList[iType]: 612 iList[iType][ticker]["type"] = iType 613 614 if "minPriceIncrement" in iList[iType][ticker].keys(): 615 iList[iType][ticker]["step"] = NanoToFloat( 616 iList[iType][ticker]["minPriceIncrement"]["units"], 617 iList[iType][ticker]["minPriceIncrement"]["nano"], 618 ) 619 620 else: 621 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 622 623 return iList 624 625 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 626 """ 627 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 628 629 See also: `DumpInstruments()`, `Listing()`. 630 631 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 632 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 633 """ 634 if self.iListDumpFile is None or not self.iListDumpFile: 635 uLogger.error("Output name of dump file must be defined!") 636 raise Exception("Filename required") 637 638 if not self.iList or forceUpdate: 639 self.iList = self.Listing() 640 641 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 642 643 # Save as XLSX with separated sheets for every type of instruments: 644 with pd.ExcelWriter( 645 path=xlsxDumpFile, 646 date_format=TKS_DATE_FORMAT, 647 datetime_format=TKS_DATE_TIME_FORMAT, 648 mode="w", 649 ) as writer: 650 for iType in TKS_INSTRUMENTS: 651 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 652 df = df[sorted(df)] # sorted by column names 653 df = df.applymap( 654 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 655 na_action="ignore", 656 ) # converting numbers from nano-type to float in every cell 657 df.to_excel( 658 writer, 659 sheet_name=iType, 660 encoding="UTF-8", 661 freeze_panes=(1, 1), 662 ) # saving as XLSX-file with freeze first row and column as headers 663 664 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 665 666 def DumpInstruments(self, forceUpdate: bool = True) -> str: 667 """ 668 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 669 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 670 671 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 672 673 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 674 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 675 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 676 """ 677 if self.iListDumpFile is None or not self.iListDumpFile: 678 uLogger.error("Output name of dump file must be defined!") 679 raise Exception("Filename required") 680 681 if not self.iList or forceUpdate: 682 self.iList = self.Listing() 683 684 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 685 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 686 fH.write(jsonDump) 687 688 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 689 690 return jsonDump 691 692 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 693 """ 694 Show information about one instrument defined by json data and prints it in Markdown format. 695 696 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 697 698 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 699 :param show: if `True` then also printing information about instrument and its current price. 700 :return: multilines text in Markdown format with information about one instrument. 701 """ 702 splitLine = "| | |\n" 703 infoText = "" 704 705 if iJSON is not None and iJSON and isinstance(iJSON, dict): 706 info = [ 707 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 708 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 709 "| Parameters | Values |\n", 710 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 711 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 712 "| Full name: | {:<54} |\n".format(iJSON["name"]), 713 ] 714 715 if "sector" in iJSON.keys() and iJSON["sector"]: 716 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 717 718 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 719 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 720 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 721 ))) 722 723 info.extend([ 724 splitLine, 725 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 726 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 727 ]) 728 729 if "isin" in iJSON.keys() and iJSON["isin"]: 730 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 731 732 if "classCode" in iJSON.keys(): 733 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 734 735 info.extend([ 736 splitLine, 737 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 738 splitLine, 739 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 740 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 741 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 742 ]) 743 744 if iJSON["figi"]: 745 self.figi = iJSON["figi"] 746 iJSON = iJSON | self.RequestTradingStatus() 747 748 info.extend([ 749 splitLine, 750 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 751 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 752 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 753 ]) 754 755 info.append(splitLine) 756 757 if "type" in iJSON.keys() and iJSON["type"]: 758 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 759 760 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 761 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 762 763 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 764 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 765 766 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 767 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 768 769 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 770 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 771 772 if "focusType" in iJSON.keys() and iJSON["focusType"]: 773 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 774 775 if "assetType" in iJSON.keys() and iJSON["assetType"]: 776 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 777 778 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 779 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 780 781 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 782 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 783 784 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 785 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 786 787 if "currency" in iJSON.keys(): 788 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 789 790 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 791 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 792 793 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 794 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 795 796 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 797 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 800 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 801 802 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 803 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 804 805 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 806 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 807 808 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 809 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 810 811 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 812 info.append("| Perpetual bond: | Yes |\n") 813 814 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 815 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 816 817 iExt = None 818 if iJSON["type"] == "Bonds": 819 info.extend([ 820 splitLine, 821 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 822 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 823 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 824 iJSON["nominal"]["currency"], 825 )), 826 ]) 827 828 if "floatingCouponFlag" in iJSON.keys(): 829 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 830 831 if "amortizationFlag" in iJSON.keys(): 832 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 833 834 info.append(splitLine) 835 836 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 837 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 838 839 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 840 841 info.extend([ 842 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 843 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 844 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 845 ]) 846 847 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 848 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 849 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 850 iJSON["aciValue"]["currency"] 851 ))) 852 853 if "currentPrice" in iJSON.keys(): 854 info.append(splitLine) 855 856 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 857 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 858 859 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 860 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 861 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 862 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 863 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 864 865 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 866 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 867 868 info.extend([ 869 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 870 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 871 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 872 )), 873 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 874 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 875 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 876 )), 877 "| Changes between last deal price and last close | {:<54} |\n".format( 878 "{:.2f}%{}".format( 879 iJSON["currentPrice"]["changes"], 880 " ({}{:.2f} {})".format( 881 "+" if bondChangesDelta > 0 else "", 882 bondChangesDelta, 883 aciCurrency 884 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 885 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 886 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 887 currency 888 ), 889 ) 890 ), 891 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 892 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 893 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 894 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 897 )), 898 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 899 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 900 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 901 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 903 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 904 )), 905 ]) 906 907 if "lot" in iJSON.keys(): 908 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 909 910 if "step" in iJSON.keys() and iJSON["step"] != 0: 911 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 912 913 # Add bond payment calendar: 914 if iJSON["type"] == "Bonds": 915 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 916 info.extend(["\n", strCalendar]) 917 918 infoText += "".join(info) 919 920 if show: 921 uLogger.info("{}".format(infoText)) 922 923 else: 924 uLogger.debug("{}".format(infoText)) 925 926 if self.infoFile is not None: 927 with open(self.infoFile, "w", encoding="UTF-8") as fH: 928 fH.write(infoText) 929 930 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 931 932 return infoText 933 934 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 935 """ 936 Search and return raw broker's information about instrument by its ticker. 937 `ticker` must be defined! If debug=True then print all debug messages. 938 939 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 940 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 941 :param debug: if `True` then print all debug console messages. 942 :return: JSON formatted data with information about instrument. 943 """ 944 tickerJSON = {} 945 if debug: 946 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 947 948 if not self.ticker: 949 uLogger.warning("self.ticker variable is not be empty!") 950 951 else: 952 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 953 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 954 raise Exception("Instrument not allowed") 955 956 if not self.iList: 957 self.iList = self.Listing() 958 959 if self.ticker in self.iList["Shares"].keys(): 960 tickerJSON = self.iList["Shares"][self.ticker] 961 if debug: 962 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 963 964 elif self.ticker in self.iList["Currencies"].keys(): 965 tickerJSON = self.iList["Currencies"][self.ticker] 966 if debug: 967 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 968 969 elif self.ticker in self.iList["Bonds"].keys(): 970 tickerJSON = self.iList["Bonds"][self.ticker] 971 if debug: 972 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 973 974 elif self.ticker in self.iList["Etfs"].keys(): 975 tickerJSON = self.iList["Etfs"][self.ticker] 976 if debug: 977 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 978 979 elif self.ticker in self.iList["Futures"].keys(): 980 tickerJSON = self.iList["Futures"][self.ticker] 981 if debug: 982 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 983 984 if tickerJSON: 985 self.figi = tickerJSON["figi"] 986 987 if requestPrice: 988 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 989 990 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 991 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 992 993 else: 994 tickerJSON["currentPrice"]["changes"] = 0 995 996 if show: 997 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 998 999 else: 1000 if show: 1001 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1002 1003 return tickerJSON 1004 1005 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1006 """ 1007 Search and return raw broker's information about instrument by its FIGI. 1008 `figi` must be defined! If debug=True then print all debug messages. 1009 1010 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1011 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1012 :param debug: if `True` then print all debug console messages. 1013 :return: JSON formatted data with information about instrument. 1014 """ 1015 figiJSON = {} 1016 if debug: 1017 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1018 1019 if not self.figi: 1020 uLogger.warning("self.figi variable is not be empty!") 1021 1022 else: 1023 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1024 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1025 raise Exception("Instrument not allowed") 1026 1027 if not self.iList: 1028 self.iList = self.Listing() 1029 1030 for item in self.iList["Shares"].keys(): 1031 if self.figi == self.iList["Shares"][item]["figi"]: 1032 figiJSON = self.iList["Shares"][item] 1033 1034 if debug: 1035 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1036 1037 break 1038 1039 if not figiJSON: 1040 for item in self.iList["Currencies"].keys(): 1041 if self.figi == self.iList["Currencies"][item]["figi"]: 1042 figiJSON = self.iList["Currencies"][item] 1043 1044 if debug: 1045 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1046 1047 break 1048 1049 if not figiJSON: 1050 for item in self.iList["Bonds"].keys(): 1051 if self.figi == self.iList["Bonds"][item]["figi"]: 1052 figiJSON = self.iList["Bonds"][item] 1053 1054 if debug: 1055 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1056 1057 break 1058 1059 if not figiJSON: 1060 for item in self.iList["Etfs"].keys(): 1061 if self.figi == self.iList["Etfs"][item]["figi"]: 1062 figiJSON = self.iList["Etfs"][item] 1063 1064 if debug: 1065 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1066 1067 break 1068 1069 if not figiJSON: 1070 for item in self.iList["Futures"].keys(): 1071 if self.figi == self.iList["Futures"][item]["figi"]: 1072 figiJSON = self.iList["Futures"][item] 1073 1074 if debug: 1075 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1076 1077 break 1078 1079 if figiJSON: 1080 self.figi = figiJSON["figi"] 1081 self.ticker = figiJSON["ticker"] 1082 1083 if requestPrice: 1084 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1085 1086 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1087 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1088 1089 else: 1090 figiJSON["currentPrice"]["changes"] = 0 1091 1092 if show: 1093 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1094 1095 else: 1096 if show: 1097 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1098 1099 return figiJSON 1100 1101 def GetCurrentPrices(self, show: bool = True) -> dict: 1102 """ 1103 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1104 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1105 1106 See also: 1107 1108 :param show: if `True` then print DOM to log and console. 1109 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1110 """ 1111 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1112 1113 if self.depth < 1: 1114 uLogger.error("Depth of Market (DOM) must be >=1!") 1115 raise Exception("Incorrect value") 1116 1117 if not (self.ticker or self.figi): 1118 uLogger.error("self.ticker or self.figi variables must be defined!") 1119 raise Exception("Ticker or FIGI required") 1120 1121 if self.ticker and not self.figi: 1122 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1123 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1124 1125 if not self.ticker and self.figi: 1126 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1127 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1128 1129 if not self.figi: 1130 uLogger.error("FIGI is not defined!") 1131 raise Exception("Ticker or FIGI required") 1132 1133 else: 1134 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1135 1136 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1137 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1138 self.body = str({"figi": self.figi, "depth": self.depth}) 1139 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1140 1141 if pricesResponse: 1142 # list of dicts with sellers orders: 1143 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1144 1145 # list of dicts with buyers orders: 1146 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1147 1148 # max price of instrument at this time: 1149 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1150 1151 # min price of instrument at this time: 1152 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1153 1154 # last price of deal with instrument: 1155 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1156 1157 # last close price of instrument: 1158 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1159 1160 else: 1161 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1162 uLogger.debug("Server response: {}".format(pricesResponse)) 1163 1164 if show: 1165 if prices["buy"] or prices["sell"]: 1166 info = [ 1167 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1168 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1169 self.ticker, 1170 self.figi, 1171 self.depth, 1172 ), 1173 uLog.sepShort, "\n", 1174 " Orders of Buyers | Orders of Sellers\n", 1175 uLog.sepShort, "\n", 1176 " Sell prices (vol.) | Buy prices (vol.)\n", 1177 uLog.sepShort, "\n", 1178 ] 1179 1180 if not prices["buy"]: 1181 info.append(" | No orders!\n") 1182 sumBuy = 0 1183 1184 else: 1185 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1186 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1187 for item in maxMinSorted: 1188 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1189 1190 if not prices["sell"]: 1191 info.append("No orders! |\n") 1192 sumSell = 0 1193 1194 else: 1195 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1196 for item in prices["sell"]: 1197 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1198 1199 info.extend([ 1200 uLog.sepShort, "\n", 1201 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1202 uLog.sepShort, "\n", 1203 ]) 1204 1205 infoText = "".join(info) 1206 1207 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1208 1209 else: 1210 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1211 1212 return prices 1213 1214 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1215 """ 1216 This method get and show information about all available broker instruments for current user account. 1217 If `instrumentsFile` string is not empty then also save information to this file. 1218 1219 :param show: if `True` then print results to console, if `False` - print only to file. 1220 :return: multi-lines string with all available broker instruments 1221 """ 1222 if not self.iList: 1223 self.iList = self.Listing() 1224 1225 info = [ 1226 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1227 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1228 ] 1229 1230 # add instruments count by type: 1231 for iType in self.iList.keys(): 1232 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1233 1234 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1235 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1236 1237 # generating info tables with all instruments by type: 1238 for iType in self.iList.keys(): 1239 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1240 1241 for instrument in self.iList[iType].keys(): 1242 iName = self.iList[iType][instrument]["name"] # instrument's name 1243 if len(iName) > 57: 1244 iName = "{}...".format(iName[:54]) # right trim for a long string 1245 1246 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1247 self.iList[iType][instrument]["ticker"], 1248 iName, 1249 self.iList[iType][instrument]["figi"], 1250 self.iList[iType][instrument]["currency"], 1251 self.iList[iType][instrument]["lot"], 1252 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1253 )) 1254 1255 infoText = "".join(info) 1256 1257 if show: 1258 uLogger.info(infoText) 1259 1260 if self.instrumentsFile: 1261 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1262 fH.write(infoText) 1263 1264 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1265 1266 return infoText 1267 1268 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1269 """ 1270 This method search and show information about instruments by part of its ticker, FIGI or name. 1271 If `searchResultsFile` string is not empty then also save information to this file. 1272 1273 :param pattern: string with part of ticker, FIGI or instrument's name. 1274 :param show: if `True` then print results to console, if `False` - return list of result only. 1275 :return: list of dictionaries with all found instruments. 1276 """ 1277 if not self.iList: 1278 self.iList = self.Listing() 1279 1280 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1281 compiledPattern = re.compile(pattern, re.IGNORECASE) 1282 1283 for iType in self.iList: 1284 for instrument in self.iList[iType].values(): 1285 searchResult = compiledPattern.search(" ".join( 1286 [instrument["ticker"], instrument["figi"], instrument["name"]] 1287 )) 1288 1289 if searchResult: 1290 searchResults[iType][instrument["ticker"]] = instrument 1291 1292 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1293 info = [ 1294 "# Search results\n\n", 1295 "* **Search pattern:** [{}]\n".format(pattern), 1296 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1297 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1298 ] 1299 infoShort = info[:] 1300 1301 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1302 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1303 skippedLine = "| ... | ... | ... | ... |\n" 1304 1305 if resultsLen == 0: 1306 info.append("\nNo results\n") 1307 infoShort.append("\nNo results\n") 1308 uLogger.warning("No results. Try changing your search pattern.") 1309 1310 else: 1311 for iType in searchResults: 1312 iTypeValuesCount = len(searchResults[iType].values()) 1313 if iTypeValuesCount > 0: 1314 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1315 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1316 1317 for instrument in searchResults[iType].values(): 1318 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1319 instrument["type"], 1320 instrument["ticker"], 1321 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1322 instrument["figi"], 1323 )) 1324 1325 if iTypeValuesCount <= 5: 1326 infoShort.extend(info[-iTypeValuesCount:]) 1327 1328 else: 1329 infoShort.extend(info[-5:]) 1330 infoShort.append(skippedLine) 1331 1332 infoText = "".join(info) 1333 infoTextShort = "".join(infoShort) 1334 1335 if show: 1336 uLogger.info(infoTextShort) 1337 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1338 1339 if self.searchResultsFile: 1340 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1341 fH.write(infoText) 1342 1343 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1344 1345 return searchResults 1346 1347 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1348 """ 1349 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1350 1351 :param instruments: list of strings with tickers or FIGIs. 1352 :return: list with unique instrument FIGIs only. 1353 """ 1354 requestedInstruments = [] 1355 for iName in instruments: 1356 if iName not in self.aliases.keys(): 1357 if iName not in requestedInstruments: 1358 requestedInstruments.append(iName) 1359 1360 else: 1361 if iName not in requestedInstruments: 1362 if self.aliases[iName] not in requestedInstruments: 1363 requestedInstruments.append(self.aliases[iName]) 1364 1365 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1366 1367 onlyUniqueFIGIs = [] 1368 for iName in requestedInstruments: 1369 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1370 continue 1371 1372 self.ticker = iName 1373 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1374 1375 if not iData: 1376 self.ticker = "" 1377 self.figi = iName 1378 1379 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1380 1381 if not iData: 1382 self.figi = "" 1383 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1384 1385 if iData and iData["figi"] not in onlyUniqueFIGIs: 1386 onlyUniqueFIGIs.append(iData["figi"]) 1387 1388 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1389 1390 return onlyUniqueFIGIs 1391 1392 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1393 """ 1394 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1395 See limits: https://tinkoff.github.io/investAPI/limits/ 1396 If `pricesFile` string is not empty then also save information to this file. 1397 1398 :param instruments: list of strings with tickers or FIGIs. 1399 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1400 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1401 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1402 """ 1403 if instruments is None or not instruments: 1404 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1405 raise Exception("Ticker or FIGI required") 1406 1407 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1408 1409 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1410 1411 iList = [] # trying to get info and current prices about all unique instruments: 1412 for self.figi in onlyUniqueFIGIs: 1413 iData = self.SearchByFIGI(requestPrice=True) 1414 iList.append(iData) 1415 1416 self.ShowListOfPrices(iList, show) 1417 1418 return iList 1419 1420 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1421 """ 1422 Show table contains current prices of given instruments. 1423 1424 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1425 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1426 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1427 :return: multilines text in Markdown format as a table contains current prices. 1428 """ 1429 infoText = "" 1430 1431 if show or self.pricesFile: 1432 info = [ 1433 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1434 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1435 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1436 ] 1437 1438 for item in iList: 1439 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1440 item["ticker"], 1441 item["figi"], 1442 item["type"], 1443 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1444 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1445 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1446 "{} / {}".format( 1447 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1448 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1449 ), 1450 "{} / {}".format( 1451 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1452 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1453 ), 1454 item["currency"], 1455 )) 1456 1457 infoText = "".join(info) 1458 1459 if show: 1460 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1461 1462 if self.pricesFile: 1463 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1464 fH.write(infoText) 1465 1466 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1467 1468 return infoText 1469 1470 def RequestTradingStatus(self) -> dict: 1471 """ 1472 Requesting trading status for the instrument defined by `figi` variable. 1473 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1474 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1475 1476 :return: dictionary with trading status attributes. Response example: 1477 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1478 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1479 """ 1480 if self.figi is None or not self.figi: 1481 uLogger.error("Variable `figi` must be defined for using this method!") 1482 raise Exception("FIGI required") 1483 1484 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1485 1486 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1487 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1488 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1489 1490 uLogger.debug("Records about current trading status successfully received") 1491 1492 return tradingStatus 1493 1494 def RequestPortfolio(self) -> dict: 1495 """ 1496 Requesting actual user's portfolio for current `accountId`. 1497 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1498 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1499 1500 :return: dictionary with user's portfolio. 1501 """ 1502 if self.accountId is None or not self.accountId: 1503 uLogger.error("Variable `accountId` must be defined for using this method!") 1504 raise Exception("Account ID required") 1505 1506 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1507 1508 self.body = str({"accountId": self.accountId}) 1509 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1510 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1511 1512 uLogger.debug("Records about user's portfolio successfully received") 1513 1514 return rawPortfolio 1515 1516 def RequestPositions(self) -> dict: 1517 """ 1518 Requesting open positions by currencies and instruments for current `accountId`. 1519 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1520 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1521 1522 :return: dictionary with open positions by instruments. 1523 """ 1524 if self.accountId is None or not self.accountId: 1525 uLogger.error("Variable `accountId` must be defined for using this method!") 1526 raise Exception("Account ID required") 1527 1528 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1529 1530 self.body = str({"accountId": self.accountId}) 1531 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1532 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1533 1534 uLogger.debug("Records about current open positions successfully received") 1535 1536 return rawPositions 1537 1538 def RequestPendingOrders(self) -> list: 1539 """ 1540 Requesting current actual pending orders for current `accountId`. 1541 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1542 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1543 1544 :return: list of dictionaries with pending orders. 1545 """ 1546 if self.accountId is None or not self.accountId: 1547 uLogger.error("Variable `accountId` must be defined for using this method!") 1548 raise Exception("Account ID required") 1549 1550 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1551 1552 self.body = str({"accountId": self.accountId}) 1553 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1554 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1555 1556 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1557 1558 return rawOrders 1559 1560 def RequestStopOrders(self) -> list: 1561 """ 1562 Requesting current actual stop orders for current `accountId`. 1563 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1564 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1565 1566 :return: list of dictionaries with stop orders. 1567 """ 1568 if self.accountId is None or not self.accountId: 1569 uLogger.error("Variable `accountId` must be defined for using this method!") 1570 raise Exception("Account ID required") 1571 1572 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1573 1574 self.body = str({"accountId": self.accountId}) 1575 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1576 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1577 1578 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1579 1580 return rawStopOrders 1581 1582 def Overview(self, show: bool = False, details: str = "full") -> dict: 1583 """ 1584 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1585 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1586 are defined then also save information to file. 1587 1588 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1589 many requests about the state of the portfolio, and then, based on the received data, a large number 1590 of calculation and statistics are collected. 1591 1592 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1593 :param details: how detailed should the information be? You should specify one of strings: 1594 `full` - shows full available information about portfolio status (by default), 1595 `positions` - shows only open positions, 1596 `digest` - show a short digest of the portfolio status, 1597 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1598 `orders` - shows only sections of open limits and stop orders. 1599 :return: dictionary with client's raw portfolio and some statistics. 1600 """ 1601 if self.accountId is None or not self.accountId: 1602 uLogger.error("Variable `accountId` must be defined for using this method!") 1603 raise Exception("Account ID required") 1604 1605 view = { 1606 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1607 "headers": {}, # list of dictionaries, response headers without "positions" section 1608 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1609 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1610 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1611 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1612 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1613 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1614 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1615 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1616 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1617 }, 1618 "stat": { # --- some statistics calculated using "raw" sections: 1619 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1620 "availableRUB": 0., # available rubles (without other currencies) 1621 "blockedRUB": 0., # blocked sum in Russian Rouble 1622 "totalChangesRUB": 0., # changes for all open trades in RUB 1623 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1624 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1625 "sharesCostRUB": 0., # costs of all shares in RUB 1626 "bondsCostRUB": 0., # costs of all bonds in RUB 1627 "etfsCostRUB": 0., # costs of all etfs in RUB 1628 "futuresCostRUB": 0., # costs of all futures in RUB 1629 "Currencies": [], # list of dictionaries of all currencies statistics 1630 "Shares": [], # list of dictionaries of all shares statistics 1631 "Bonds": [], # list of dictionaries of all bonds statistics 1632 "Etfs": [], # list of dictionaries of all etfs statistics 1633 "Futures": [], # list of dictionaries of all futures statistics 1634 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1635 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1636 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1637 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1638 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1639 }, 1640 "analytics": { # --- some analytics of portfolio: 1641 "distrByAssets": {}, # portfolio distribution by assets 1642 "distrByCompanies": {}, # portfolio distribution by companies 1643 "distrBySectors": {}, # portfolio distribution by sectors 1644 "distrByCurrencies": {}, # portfolio distribution by currencies 1645 "distrByCountries": {}, # portfolio distribution by countries 1646 } 1647 } 1648 1649 details = details.lower() 1650 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1651 if details not in availableDetails: 1652 details = "full" 1653 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1654 1655 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1656 1657 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1658 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1659 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1660 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1661 1662 # save response headers without "positions" section: 1663 for key in portfolioResponse.keys(): 1664 if key != "positions": 1665 view["raw"]["headers"][key] = portfolioResponse[key] 1666 1667 else: 1668 continue 1669 1670 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1671 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1672 for item in portfolioResponse["positions"]: 1673 if item["instrumentType"] == "currency": 1674 self.figi = item["figi"] 1675 curr = self.SearchByFIGI(requestPrice=False) 1676 1677 # current price of currency in RUB: 1678 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1679 "name": curr["name"], 1680 "currentPrice": NanoToFloat( 1681 item["currentPrice"]["units"], 1682 item["currentPrice"]["nano"] 1683 ), 1684 } 1685 1686 view["raw"]["Currencies"].append(item) 1687 1688 elif item["instrumentType"] == "share": 1689 view["raw"]["Shares"].append(item) 1690 1691 elif item["instrumentType"] == "bond": 1692 view["raw"]["Bonds"].append(item) 1693 1694 elif item["instrumentType"] == "etf": 1695 view["raw"]["Etfs"].append(item) 1696 1697 elif item["instrumentType"] == "futures": 1698 view["raw"]["Futures"].append(item) 1699 1700 else: 1701 continue 1702 1703 # how many volume of currencies (by ISO currency name) are blocked: 1704 for item in view["raw"]["positions"]["blocked"]: 1705 blocked = NanoToFloat(item["units"], item["nano"]) 1706 if blocked > 0: 1707 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1708 1709 # how many volume of instruments (by FIGI) are blocked: 1710 for item in view["raw"]["positions"]["securities"]: 1711 blocked = int(item["blocked"]) 1712 if blocked > 0: 1713 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1714 1715 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1716 1717 if "rub" in allBlocked.keys(): 1718 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1719 1720 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1721 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1722 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1723 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1724 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1725 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1726 view["stat"]["portfolioCostRUB"] = sum([ 1727 view["stat"]["allCurrenciesCostRUB"], 1728 view["stat"]["sharesCostRUB"], 1729 view["stat"]["bondsCostRUB"], 1730 view["stat"]["etfsCostRUB"], 1731 view["stat"]["futuresCostRUB"], 1732 ]) 1733 1734 # --- calculating some portfolio statistics: 1735 byComp = {} # distribution by companies 1736 bySect = {} # distribution by sectors 1737 byCurr = {} # distribution by currencies (include RUB) 1738 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1739 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1740 1741 for item in portfolioResponse["positions"]: 1742 self.figi = item["figi"] 1743 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1744 1745 if instrument: 1746 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1747 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1748 1749 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1750 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1751 1752 else: 1753 blocked = 0 1754 1755 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1756 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1757 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1758 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1759 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1760 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1761 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1762 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1763 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1764 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1765 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1766 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1767 1768 statData = { 1769 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1770 "ticker": instrument["ticker"], # ticker by FIGI 1771 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1772 "volume": volume, # available volume of instrument 1773 "lots": lots, # volume in lots of instrument 1774 "direction": direction, # direction of an instrument's position: short or long 1775 "blocked": blocked, # blocked volume of currency or instrument 1776 "currentPrice": curPrice, # current instrument's price in basic asset 1777 "average": average, # current average position price 1778 "cost": cost, # current cost of all volume of instrument in basic asset 1779 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1780 "costRUB": costRUB, # cost of instrument in ruble 1781 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1782 "profit": profit, # expected profit at current moment 1783 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1784 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1785 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1786 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1787 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1788 "step": instrument["step"], # minimum price increment 1789 } 1790 1791 # adding distribution by unique countries: 1792 if statData["country"] not in byCountry.keys(): 1793 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1794 1795 else: 1796 byCountry[statData["country"]]["cost"] += costRUB 1797 byCountry[statData["country"]]["percent"] += percentCostRUB 1798 1799 if item["instrumentType"] != "currency": 1800 # adding distribution by unique companies: 1801 if statData["name"]: 1802 if statData["name"] not in byComp.keys(): 1803 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1804 1805 else: 1806 byComp[statData["name"]]["cost"] += costRUB 1807 byComp[statData["name"]]["percent"] += percentCostRUB 1808 1809 # adding distribution by unique sectors: 1810 if statData["sector"] not in bySect.keys(): 1811 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1812 1813 else: 1814 bySect[statData["sector"]]["cost"] += costRUB 1815 bySect[statData["sector"]]["percent"] += percentCostRUB 1816 1817 # adding distribution by unique currencies: 1818 if currency not in byCurr.keys(): 1819 byCurr[currency] = { 1820 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1821 "cost": costRUB, 1822 "percent": percentCostRUB 1823 } 1824 1825 else: 1826 byCurr[currency]["cost"] += costRUB 1827 byCurr[currency]["percent"] += percentCostRUB 1828 1829 # saving statistics for every instrument: 1830 if item["instrumentType"] == "currency": 1831 view["stat"]["Currencies"].append(statData) 1832 1833 # update dict with free funds for trading (total - blocked) by currencies 1834 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1835 view["stat"]["funds"][currency] = { 1836 "total": volume, 1837 "totalCostRUB": costRUB, # total volume cost in rubles 1838 "free": volume - blocked, 1839 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1840 } 1841 1842 elif item["instrumentType"] == "share": 1843 view["stat"]["Shares"].append(statData) 1844 1845 elif item["instrumentType"] == "bond": 1846 view["stat"]["Bonds"].append(statData) 1847 1848 elif item["instrumentType"] == "etf": 1849 view["stat"]["Etfs"].append(statData) 1850 1851 elif item["instrumentType"] == "Futures": 1852 view["stat"]["Futures"].append(statData) 1853 1854 else: 1855 continue 1856 1857 # total changes in Russian Ruble: 1858 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1859 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1860 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1861 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1862 view["stat"]["funds"]["rub"] = { 1863 "total": view["stat"]["availableRUB"], 1864 "totalCostRUB": view["stat"]["availableRUB"], 1865 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1866 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1867 } 1868 1869 # --- pending orders sector data: 1870 uniquePendingOrders = [] 1871 uniquePendingOrdersFIGIs = [] 1872 for item in view["raw"]["orders"]: 1873 if item["figi"] not in uniquePendingOrdersFIGIs: 1874 uniquePendingOrdersFIGIs.append(item["figi"]) 1875 uniquePendingOrders.append(item) 1876 1877 for item in uniquePendingOrders: 1878 self.figi = item["figi"] 1879 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1880 1881 if instrument: 1882 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1883 orderType = TKS_ORDER_TYPES[item["orderType"]] 1884 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1885 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1886 1887 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1888 if item["direction"] == "ORDER_DIRECTION_BUY": 1889 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1890 1891 else: 1892 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1893 1894 # requested price for order execution: 1895 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1896 1897 # necessary changes in percent to reach target from current price: 1898 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1899 1900 view["stat"]["orders"].append({ 1901 "orderID": item["orderId"], # orderId number parameter of current order 1902 "figi": item["figi"], # FIGI identification 1903 "ticker": instrument["ticker"], # ticker name by FIGI 1904 "lotsRequested": item["lotsRequested"], # requested lots value 1905 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1906 "currentPrice": lastPrice, # current instrument's price for defined action 1907 "targetPrice": target, # requested price for order execution in base currency 1908 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1909 "percentChanges": changes, # changes in percent to target from current price 1910 "currency": item["currency"], # instrument's currency name 1911 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1912 "type": orderType, # type of order from TKS_ORDER_TYPES 1913 "status": orderState, # order status from TKS_ORDER_STATES 1914 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1915 }) 1916 1917 # --- stop orders sector data: 1918 uniqueStopOrders = [] 1919 uniqueStopOrdersFIGIs = [] 1920 for item in view["raw"]["stopOrders"]: 1921 if item["figi"] not in uniqueStopOrdersFIGIs: 1922 uniqueStopOrdersFIGIs.append(item["figi"]) 1923 uniqueStopOrders.append(item) 1924 1925 for item in uniqueStopOrders: 1926 self.figi = item["figi"] 1927 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1928 1929 if instrument: 1930 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1931 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1932 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1933 1934 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1935 if "expirationTime" in item.keys(): 1936 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1937 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1938 1939 else: 1940 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1941 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1942 1943 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1944 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1945 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1946 1947 else: 1948 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1949 1950 # requested price when stop-order executed: 1951 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1952 1953 # price for limit-order, set up when stop-order executed: 1954 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1955 1956 # necessary changes in percent to reach target from current price: 1957 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1958 1959 view["stat"]["stopOrders"].append({ 1960 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1961 "figi": item["figi"], # FIGI identification 1962 "ticker": instrument["ticker"], # ticker name by FIGI 1963 "lotsRequested": item["lotsRequested"], # requested lots value 1964 "currentPrice": lastPrice, # current instrument's price for defined action 1965 "targetPrice": target, # requested price for stop-order execution in base currency 1966 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1967 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1968 "percentChanges": changes, # changes in percent to target from current price 1969 "currency": item["currency"], # instrument's currency name 1970 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1971 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1972 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1973 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1974 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1975 }) 1976 1977 # --- calculating data for analytics section: 1978 # portfolio distribution by assets: 1979 view["analytics"]["distrByAssets"] = { 1980 "Ruble": { 1981 "uniques": 1, 1982 "cost": view["stat"]["availableRUB"], 1983 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1984 }, 1985 "Currencies": { 1986 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1987 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1988 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1989 }, 1990 "Shares": { 1991 "uniques": len(view["stat"]["Shares"]), 1992 "cost": view["stat"]["sharesCostRUB"], 1993 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1994 }, 1995 "Bonds": { 1996 "uniques": len(view["stat"]["Bonds"]), 1997 "cost": view["stat"]["bondsCostRUB"], 1998 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1999 }, 2000 "Etfs": { 2001 "uniques": len(view["stat"]["Etfs"]), 2002 "cost": view["stat"]["etfsCostRUB"], 2003 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2004 }, 2005 "Futures": { 2006 "uniques": len(view["stat"]["Futures"]), 2007 "cost": view["stat"]["futuresCostRUB"], 2008 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 } 2011 2012 # portfolio distribution by companies: 2013 view["analytics"]["distrByCompanies"]["All money cash"] = { 2014 "ticker": "", 2015 "cost": view["stat"]["allCurrenciesCostRUB"], 2016 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2017 } 2018 view["analytics"]["distrByCompanies"].update(byComp) 2019 2020 # portfolio distribution by sectors: 2021 view["analytics"]["distrBySectors"]["All money cash"] = { 2022 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2023 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2024 } 2025 view["analytics"]["distrBySectors"].update(bySect) 2026 2027 # portfolio distribution by currencies: 2028 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2029 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2030 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2031 2032 view["analytics"]["distrByCurrencies"].update(byCurr) 2033 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2034 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2035 2036 # portfolio distribution by countries: 2037 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2038 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2039 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2040 2041 view["analytics"]["distrByCountries"].update(byCountry) 2042 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2043 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2044 2045 # --- Prepare text statistics overview in human-readable: 2046 if show: 2047 # Whatever the value `details`, header not changes: 2048 info = [ 2049 "# Client's portfolio\n\n", 2050 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2051 "* **Account ID:** [{}]\n".format(self.accountId), 2052 ] 2053 2054 if details in ["full", "positions", "digest"]: 2055 info.extend([ 2056 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2057 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2058 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2059 view["stat"]["totalChangesRUB"], 2060 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2061 view["stat"]["totalChangesPercentRUB"], 2062 ), 2063 ]) 2064 2065 if details in ["full", "positions"]: 2066 info.extend([ 2067 "## Open positions\n\n", 2068 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2069 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2070 "| Ruble | {:>31} | | | | | |\n".format( 2071 "{:.2f} ({:.2f}) rub".format( 2072 view["stat"]["availableRUB"], 2073 view["stat"]["blockedRUB"], 2074 ) 2075 ) 2076 ]) 2077 2078 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2079 return [ 2080 "| | | | | | | |\n", 2081 "| {:<27} | | | | | {:>19} | |\n".format( 2082 noTradeStr if noTradeStr else typeStr, 2083 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2084 ), 2085 ] 2086 2087 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2088 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2089 "{} [{}]".format(data["ticker"], data["figi"]), 2090 "{:.2f} ({:.2f}) {}".format( 2091 data["volume"], 2092 data["blocked"], 2093 data["currency"], 2094 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2095 data["volume"], 2096 data["blocked"], 2097 ), 2098 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2099 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2100 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2101 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2102 "{}{:.2f} {} ({}{:.2f}%)".format( 2103 "+" if data["profit"] > 0 else "", 2104 data["profit"], data["baseCurrencyName"], 2105 "+" if data["percentProfit"] > 0 else "", 2106 data["percentProfit"], 2107 ), 2108 ) 2109 2110 # --- Show currencies section: 2111 if view["stat"]["Currencies"]: 2112 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2113 for item in view["stat"]["Currencies"]: 2114 info.append(_InfoStr(item, showCurrencyName=True)) 2115 2116 else: 2117 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2118 2119 # --- Show shares section: 2120 if view["stat"]["Shares"]: 2121 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2122 2123 for item in view["stat"]["Shares"]: 2124 info.append(_InfoStr(item)) 2125 2126 else: 2127 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2128 2129 # --- Show bonds section: 2130 if view["stat"]["Bonds"]: 2131 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2132 2133 for item in view["stat"]["Bonds"]: 2134 info.append(_InfoStr(item)) 2135 2136 else: 2137 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2138 2139 # --- Show etfs section: 2140 if view["stat"]["Etfs"]: 2141 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2142 2143 for item in view["stat"]["Etfs"]: 2144 info.append(_InfoStr(item)) 2145 2146 else: 2147 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2148 2149 # --- Show futures section: 2150 if view["stat"]["Futures"]: 2151 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2152 2153 for item in view["stat"]["Futures"]: 2154 info.append(_InfoStr(item)) 2155 2156 else: 2157 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2158 2159 if details in ["full", "orders"]: 2160 # --- Show pending orders section: 2161 if view["stat"]["orders"]: 2162 info.extend([ 2163 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2164 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2165 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2166 ]) 2167 2168 for item in view["stat"]["orders"]: 2169 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2170 "{} [{}]".format(item["ticker"], item["figi"]), 2171 item["orderID"], 2172 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2173 "{} {} ({}{:.2f}%)".format( 2174 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2175 item["baseCurrencyName"], 2176 "+" if item["percentChanges"] > 0 else "", 2177 float(item["percentChanges"]), 2178 ), 2179 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2180 item["action"], 2181 item["type"], 2182 item["date"], 2183 )) 2184 2185 else: 2186 info.append("\n## Total pending limit-orders: 0\n") 2187 2188 # --- Show stop orders section: 2189 if view["stat"]["stopOrders"]: 2190 info.extend([ 2191 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2192 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2193 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2194 ]) 2195 2196 for item in view["stat"]["stopOrders"]: 2197 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2198 "{} [{}]".format(item["ticker"], item["figi"]), 2199 item["orderID"], 2200 item["lotsRequested"], 2201 "{} {} ({}{:.2f}%)".format( 2202 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2203 item["baseCurrencyName"], 2204 "+" if item["percentChanges"] > 0 else "", 2205 float(item["percentChanges"]), 2206 ), 2207 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2208 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2209 item["action"], 2210 item["type"], 2211 item["expType"], 2212 item["createDate"], 2213 item["expDate"], 2214 )) 2215 2216 else: 2217 info.append("\n## Total stop-orders: 0\n") 2218 2219 if details in ["full", "analytics"]: 2220 # -- Show analytics section: 2221 if view["stat"]["portfolioCostRUB"] > 0: 2222 info.extend([ 2223 "\n# Analytics\n" 2224 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2225 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2226 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2227 view["stat"]["totalChangesRUB"], 2228 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2229 view["stat"]["totalChangesPercentRUB"], 2230 ), 2231 "\n## Portfolio distribution by assets\n" 2232 "\n| Type | Uniques | Percent | Current cost |\n", 2233 "|------------|---------|---------|--------------------|\n", 2234 ]) 2235 2236 for key in view["analytics"]["distrByAssets"].keys(): 2237 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2238 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2239 key, 2240 view["analytics"]["distrByAssets"][key]["uniques"], 2241 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2242 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2243 )) 2244 2245 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2246 info.extend([ 2247 "\n## Portfolio distribution by companies\n" 2248 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2249 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2250 ]) 2251 2252 for company in view["analytics"]["distrByCompanies"].keys(): 2253 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2254 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2255 info.append("| {} | {:<7} | {:<18} |\n".format( 2256 "{}{}{}".format( 2257 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2258 company, 2259 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2260 ), 2261 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2262 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2263 )) 2264 2265 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2266 info.extend([ 2267 "\n## Portfolio distribution by sectors\n" 2268 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2269 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2270 ]) 2271 2272 for sector in view["analytics"]["distrBySectors"].keys(): 2273 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2274 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2275 sector, 2276 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2277 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2278 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2279 )) 2280 2281 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2282 info.extend([ 2283 "\n## Portfolio distribution by currencies\n" 2284 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2285 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2286 ]) 2287 2288 for curr in view["analytics"]["distrByCurrencies"].keys(): 2289 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2290 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2291 info.append("| {} | {:<7} | {:<18} |\n".format( 2292 "[{}] {}{}".format( 2293 curr, 2294 view["analytics"]["distrByCurrencies"][curr]["name"], 2295 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2296 ), 2297 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2298 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2299 )) 2300 2301 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2302 info.extend([ 2303 "\n## Portfolio distribution by countries\n" 2304 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2305 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2306 ]) 2307 2308 for country in view["analytics"]["distrByCountries"].keys(): 2309 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2310 nameLen = len(country) 2311 info.append("| {} | {:<7} | {:<18} |\n".format( 2312 "{}{}".format( 2313 country, 2314 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2315 ), 2316 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2317 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2318 )) 2319 2320 infoText = "".join(info) 2321 2322 uLogger.info(infoText) 2323 2324 if details == "full" and self.overviewFile: 2325 filename = self.overviewFile 2326 2327 elif details == "digest" and self.overviewDigestFile: 2328 filename = self.overviewDigestFile 2329 2330 elif details == "positions" and self.overviewPositionsFile: 2331 filename = self.overviewPositionsFile 2332 2333 elif details == "orders" and self.overviewOrdersFile: 2334 filename = self.overviewOrdersFile 2335 2336 elif details == "analytics" and self.overviewAnalyticsFile: 2337 filename = self.overviewAnalyticsFile 2338 2339 else: 2340 filename = "" 2341 2342 if filename: 2343 with open(filename, "w", encoding="UTF-8") as fH: 2344 fH.write(infoText) 2345 2346 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2347 2348 return view 2349 2350 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2351 """ 2352 Returns history operations between two given dates for current `accountId`. 2353 If `reportFile` string is not empty then also save human-readable report. 2354 Shows some statistical data of closed positions. 2355 2356 :param start: see docstring in `GetDatesAsString()` method 2357 :param end: see docstring in `GetDatesAsString()` method 2358 :param show: if `True` then also prints all records to the console. 2359 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2360 :return: original list of dictionaries with history of deals records from API ("operations" key): 2361 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2362 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2363 """ 2364 if self.accountId is None or not self.accountId: 2365 uLogger.error("Variable `accountId` must be defined for using this method!") 2366 raise Exception("Account ID required") 2367 2368 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2369 2370 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2371 2372 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2373 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2374 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2375 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2376 customStat = {} # custom statistics in additional to responseJSON 2377 2378 # --- output report in human-readable format: 2379 if show or self.reportFile: 2380 splitLine1 = "| | | | | |\n" # Summary section 2381 splitLine2 = "| | | | | | | | |\n" # Operations section 2382 nextDay = "" 2383 2384 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2385 2386 if len(ops) > 0: 2387 customStat = { 2388 "opsCount": 0, # total operations count 2389 "buyCount": 0, # buy operations 2390 "sellCount": 0, # sell operations 2391 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2392 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2393 "payIn": {"rub": 0.}, # Deposit brokerage account 2394 "payOut": {"rub": 0.}, # Withdrawals 2395 "divs": {"rub": 0.}, # Dividends income 2396 "coupons": {"rub": 0.}, # Coupon's income 2397 "brokerCom": {"rub": 0.}, # Service commissions 2398 "serviceCom": {"rub": 0.}, # Service commissions 2399 "marginCom": {"rub": 0.}, # Margin commissions 2400 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2401 } 2402 2403 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2404 for item in ops: 2405 if item["state"] == "OPERATION_STATE_EXECUTED": 2406 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2407 2408 # count buy operations: 2409 if "_BUY" in item["operationType"]: 2410 customStat["buyCount"] += 1 2411 2412 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2413 customStat["buyTotal"][item["payment"]["currency"]] += payment 2414 2415 else: 2416 customStat["buyTotal"][item["payment"]["currency"]] = payment 2417 2418 # count sell operations: 2419 elif "_SELL" in item["operationType"]: 2420 customStat["sellCount"] += 1 2421 2422 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2423 customStat["sellTotal"][item["payment"]["currency"]] += payment 2424 2425 else: 2426 customStat["sellTotal"][item["payment"]["currency"]] = payment 2427 2428 # count incoming operations: 2429 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2430 if item["payment"]["currency"] in customStat["payIn"].keys(): 2431 customStat["payIn"][item["payment"]["currency"]] += payment 2432 2433 else: 2434 customStat["payIn"][item["payment"]["currency"]] = payment 2435 2436 # count withdrawals operations: 2437 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2438 if item["payment"]["currency"] in customStat["payOut"].keys(): 2439 customStat["payOut"][item["payment"]["currency"]] += payment 2440 2441 else: 2442 customStat["payOut"][item["payment"]["currency"]] = payment 2443 2444 # count dividends income: 2445 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2446 if item["payment"]["currency"] in customStat["divs"].keys(): 2447 customStat["divs"][item["payment"]["currency"]] += payment 2448 2449 else: 2450 customStat["divs"][item["payment"]["currency"]] = payment 2451 2452 # count coupon's income: 2453 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2454 if item["payment"]["currency"] in customStat["coupons"].keys(): 2455 customStat["coupons"][item["payment"]["currency"]] += payment 2456 2457 else: 2458 customStat["coupons"][item["payment"]["currency"]] = payment 2459 2460 # count broker commissions: 2461 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2462 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2463 customStat["brokerCom"][item["payment"]["currency"]] += payment 2464 2465 else: 2466 customStat["brokerCom"][item["payment"]["currency"]] = payment 2467 2468 # count service commissions: 2469 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2470 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2471 customStat["serviceCom"][item["payment"]["currency"]] += payment 2472 2473 else: 2474 customStat["serviceCom"][item["payment"]["currency"]] = payment 2475 2476 # count margin commissions: 2477 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2478 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2479 customStat["marginCom"][item["payment"]["currency"]] += payment 2480 2481 else: 2482 customStat["marginCom"][item["payment"]["currency"]] = payment 2483 2484 # count withholding taxes: 2485 elif "_TAX" in item["operationType"]: 2486 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2487 customStat["allTaxes"][item["payment"]["currency"]] += payment 2488 2489 else: 2490 customStat["allTaxes"][item["payment"]["currency"]] = payment 2491 2492 else: 2493 continue 2494 2495 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2496 2497 # --- view "Actions" lines: 2498 info.extend([ 2499 "| 1 | 2 | 3 | 4 | 5 |\n", 2500 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2501 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2502 "| | Buy: {:<22} | {:<28} | | |\n".format( 2503 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2504 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2505 ), 2506 "| | Sell: {:<21} | {:<28} | | |\n".format( 2507 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2508 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2509 ), 2510 ]) 2511 2512 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2513 for key in opsKeys: 2514 if key == "rub": 2515 continue 2516 2517 info.extend([ 2518 "| | | {:<28} | | |\n".format( 2519 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2520 ), 2521 "| | | {:<28} | | |\n".format( 2522 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2523 ), 2524 ]) 2525 2526 info.append(splitLine1) 2527 2528 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2529 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2530 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2531 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2532 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2533 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2534 ) 2535 2536 # --- view "Payments" lines: 2537 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2538 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2539 2540 for key in paymentsKeys: 2541 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2542 2543 info.append(splitLine1) 2544 2545 # --- view "Commissions and taxes" lines: 2546 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2547 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2548 2549 for key in comKeys: 2550 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2551 2552 info.append(splitLine1) 2553 2554 info.extend([ 2555 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2556 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2557 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2558 ]) 2559 2560 else: 2561 info.append("Broker returned no operations during this period\n") 2562 2563 # --- view "Operations" section: 2564 for item in ops: 2565 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2566 continue 2567 2568 else: 2569 self.figi = item["figi"] if item["figi"] else "" 2570 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2571 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2572 2573 # group of deals during one day: 2574 if nextDay and item["date"].split("T")[0] != nextDay: 2575 info.append(splitLine2) 2576 nextDay = "" 2577 2578 else: 2579 nextDay = item["date"].split("T")[0] # saving current day for splitting 2580 2581 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2582 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2583 self.figi if self.figi else "—", 2584 instrument["ticker"] if instrument else "—", 2585 instrument["type"] if instrument else "—", 2586 item["quantity"] if int(item["quantity"]) > 0 else "—", 2587 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2588 TKS_OPERATION_STATES[item["state"]], 2589 TKS_OPERATION_TYPES[item["operationType"]], 2590 )) 2591 2592 infoText = "".join(info) 2593 2594 if show: 2595 uLogger.info(infoText) 2596 2597 if self.reportFile: 2598 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2599 fH.write(infoText) 2600 2601 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2602 2603 return ops, customStat 2604 2605 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2606 """ 2607 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2608 2609 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2610 Warning! Broker server used ISO UTC time by default. 2611 2612 If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe. 2613 Also, `historyFile` used to update history with `onlyMissing` parameter. 2614 2615 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2616 2617 :param start: see docstring in `GetDatesAsString()` method. 2618 :param end: see docstring in `GetDatesAsString()` method. 2619 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2620 `"hour"`, `"day"`. Default: `"hour"`. 2621 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2622 False by default. Warning! History appends only from last candle to current time 2623 with always update last candle! 2624 :param csvSep: separator if csv-file is used, `,` by default. 2625 :param show: if `True` then also prints pandas dataframe to the console. 2626 :return: pandas dataframe with prices history. Headers of columns are defined by default: 2627 `["date", "time", "open", "high", "low", "close", "volume"]`. 2628 """ 2629 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2630 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2631 history = None # empty pandas object for history 2632 2633 if interval not in TKS_CANDLE_INTERVALS.keys(): 2634 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2635 raise Exception("Incorrect value") 2636 2637 if not (self.ticker or self.figi): 2638 uLogger.error("Ticker or FIGI must be defined!") 2639 raise Exception("Ticker or FIGI required") 2640 2641 if self.ticker and not self.figi: 2642 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2643 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2644 2645 if self.figi and not self.ticker: 2646 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2647 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2648 2649 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2650 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2651 if interval.lower() != "day": 2652 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2653 2654 delta = dtEnd - dtStart # current UTC time minus last time in file 2655 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2656 2657 # calculate history length in candles: 2658 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2659 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2660 length += 1 # to avoid fraction time 2661 2662 # calculate data blocks count: 2663 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2664 2665 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2666 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2667 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2668 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2669 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2670 2671 tempOld = None # pandas object for old history, if --only-missing key present 2672 lastTime = None # datetime object of last old candle in file 2673 2674 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2675 uLogger.debug("--only-missing key present, add only last missing candles...") 2676 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2677 2678 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2679 2680 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2681 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2682 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2683 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2684 2685 # get last datetime object from last string in file or minus 1 delta if file is empty: 2686 if len(tempOld) > 0: 2687 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2688 2689 else: 2690 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2691 2692 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2693 2694 responseJSONs = [] # raw history blocks of data 2695 2696 blockEnd = dtEnd 2697 for item in range(blocks): 2698 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2699 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2700 2701 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2702 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2703 )) 2704 2705 if blockStart == blockEnd: 2706 uLogger.debug("Skipped this zero-length block...") 2707 2708 else: 2709 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2710 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2711 self.body = str({ 2712 "figi": self.figi, 2713 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2714 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2715 "interval": TKS_CANDLE_INTERVALS[interval][0] 2716 }) 2717 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2718 2719 if "code" in responseJSON.keys(): 2720 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2721 2722 else: 2723 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2724 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2725 2726 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2727 2728 blockEnd = blockStart 2729 2730 printCount = len(responseJSONs) # candles to show in console 2731 if responseJSONs: 2732 tempHistory = pd.DataFrame( 2733 data={ 2734 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2735 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2736 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2737 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2738 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2739 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2740 "volume": [int(item["volume"]) for item in responseJSONs], 2741 }, 2742 index=range(len(responseJSONs)), 2743 columns=["date", "time", "open", "high", "low", "close", "volume"], 2744 ) 2745 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2746 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2747 2748 # append only newest candles to old history if --only-missing key present: 2749 if onlyMissing and tempOld is not None and lastTime is not None: 2750 index = 0 # find start index in tempHistory data: 2751 2752 for i, item in tempHistory.iterrows(): 2753 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2754 2755 if curTime == lastTime: 2756 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2757 index = i 2758 printCount = index + 1 2759 break 2760 2761 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2762 2763 else: 2764 history = tempHistory # if no `--only-missing` key then load full data from server 2765 2766 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2767 2768 if history is not None and not history.empty: 2769 if show: 2770 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2771 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2772 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2773 )) 2774 2775 else: 2776 uLogger.warning("Received an empty candles history!") 2777 2778 if self.historyFile is not None: 2779 if history is not None and not history.empty: 2780 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2781 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2782 2783 else: 2784 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2785 2786 else: 2787 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.") 2788 2789 return history 2790 2791 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2792 """ 2793 Load candles history from csv-file and return pandas dataframe object. 2794 2795 See also: `History()` and `ShowHistoryChart()` methods. 2796 2797 :param filePath: path to csv-file to open. 2798 """ 2799 loadedHistory = None # init candles data object 2800 2801 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2802 2803 if os.path.exists(filePath): 2804 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as pandas dataframe 2805 2806 tfStr = self.priceModel.FormattedDelta( 2807 self.priceModel.timeframe, 2808 "{days} days {hours}h {minutes}m {seconds}s", 2809 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2810 self.priceModel.timeframe, 2811 "{hours}h {minutes}m {seconds}s", 2812 ) 2813 2814 if loadedHistory is not None and not loadedHistory.empty: 2815 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2816 len(loadedHistory), 2817 tfStr, 2818 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2819 ) 2820 2821 else: 2822 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2823 2824 else: 2825 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2826 2827 return loadedHistory 2828 2829 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2830 """ 2831 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2832 2833 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2834 Default: `index.html` (both for interact and non-interact candlesticks chart). 2835 2836 See also: `History()` and `LoadHistory()` methods. 2837 2838 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2839 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2840 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2841 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2842 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2843 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2844 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2845 """ 2846 if isinstance(candles, str): 2847 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2848 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2849 2850 elif isinstance(candles, pd.DataFrame): 2851 self.priceModel.prices = candles # set candles chain from variable 2852 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2853 2854 if "datetime" not in candles.columns: 2855 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2856 2857 else: 2858 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2859 raise Exception("Incorrect value") 2860 2861 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2862 2863 if interact: 2864 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2865 2866 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2867 2868 else: 2869 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2870 2871 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2872 2873 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2874 2875 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2876 """ 2877 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2878 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2879 2880 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2881 2882 :param operation: string "Buy" or "Sell". 2883 :param lots: volume, integer count of lots >= 1. 2884 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2885 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2886 :param expDate: string "Undefined" by default or local date in future, 2887 it is a string with format `%Y-%m-%d %H:%M:%S`. 2888 :return: JSON with response from broker server. 2889 """ 2890 if self.accountId is None or not self.accountId: 2891 uLogger.error("Variable `accountId` must be defined for using this method!") 2892 raise Exception("Account ID required") 2893 2894 if operation is None or not operation or operation not in ("Buy", "Sell"): 2895 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2896 raise Exception("Incorrect value") 2897 2898 if lots is None or lots < 1: 2899 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2900 lots = 1 2901 2902 if tp is None or tp < 0: 2903 tp = 0 2904 2905 if sl is None or sl < 0: 2906 sl = 0 2907 2908 if expDate is None or not expDate: 2909 expDate = "Undefined" 2910 2911 if not (self.ticker or self.figi): 2912 uLogger.error("Ticker or FIGI must be defined!") 2913 raise Exception("Ticker or FIGI required") 2914 2915 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2916 self.ticker = instrument["ticker"] 2917 self.figi = instrument["figi"] 2918 2919 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2920 2921 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2922 self.body = str({ 2923 "figi": self.figi, 2924 "quantity": str(lots), 2925 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2926 "accountId": str(self.accountId), 2927 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2928 }) 2929 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2930 2931 if "orderId" in response.keys(): 2932 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2933 operation, response["orderId"], 2934 self.ticker, self.figi, lots, 2935 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2936 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2937 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2938 )) 2939 2940 else: 2941 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2942 2943 if tp > 0: 2944 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2945 2946 if sl > 0: 2947 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2948 2949 return response 2950 2951 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2952 """ 2953 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2954 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2955 2956 See also: `Order()` and `Trade()` docstrings. 2957 2958 :param lots: volume, integer count of lots >= 1. 2959 :param tp: float > 0, take profit price of stop-order. 2960 :param sl: float > 0, stop loss price of stop-order. 2961 :param expDate: it's a local date in future. 2962 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2963 :return: JSON with response from broker server. 2964 """ 2965 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2966 2967 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2968 """ 2969 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2970 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2971 2972 See also: `Order()` and `Trade()` docstrings. 2973 2974 :param lots: volume, integer count of lots >= 1. 2975 :param tp: float > 0, take profit price of stop-order. 2976 :param sl: float > 0, stop loss price of stop-order. 2977 :param expDate: it's a local date in the future. 2978 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2979 :return: JSON with response from broker server. 2980 """ 2981 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2982 2983 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2984 """ 2985 Close position of given instruments. 2986 2987 :param tickers: tickers list of instruments that must be closed. 2988 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2989 This avoids unnecessary downloading data from the server. 2990 """ 2991 if not tickers: 2992 uLogger.info("Tickers list is empty, nothing to close.") 2993 2994 else: 2995 if portfolio is None or not portfolio: 2996 portfolio = self.Overview(show=False) 2997 2998 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 2999 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3000 3001 for ticker in tickers: 3002 if ticker not in allOpenedTickers: 3003 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3004 continue 3005 3006 # search open trade info about instrument by ticker: 3007 instrument = {} 3008 for iType in TKS_INSTRUMENTS: 3009 if instrument: 3010 break 3011 3012 for item in portfolio["stat"][iType]: 3013 if item["ticker"] == ticker: 3014 instrument = item 3015 break 3016 3017 if instrument: 3018 self.ticker = ticker 3019 self.figi = instrument["figi"] 3020 3021 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3022 self.ticker, 3023 self.figi, 3024 int(instrument["volume"]), 3025 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3026 )) 3027 3028 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3029 3030 if tradeLots > 0: 3031 if instrument["blocked"] > 0: 3032 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3033 instrument["blocked"], 3034 self.ticker, 3035 tradeLots, 3036 )) 3037 3038 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3039 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3040 3041 else: 3042 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3043 3044 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3045 """ 3046 Close all positions of given instruments with defined type. 3047 3048 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3049 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3050 This avoids unnecessary downloading data from the server. 3051 """ 3052 if iType not in TKS_INSTRUMENTS: 3053 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3054 3055 else: 3056 if portfolio is None or not portfolio: 3057 portfolio = self.Overview(show=False) 3058 3059 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3060 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3061 3062 if tickers and portfolio: 3063 self.CloseTrades(tickers, portfolio) 3064 3065 else: 3066 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3067 3068 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3069 """ 3070 Universal method to create market or limit orders with all available parameters for current `accountId`. 3071 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3072 3073 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3074 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3075 3076 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3077 then broker immediately open market order as you can do simple --buy or --sell operations! 3078 3079 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3080 When current price will go up or down to target price value then broker opens a limit order. 3081 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3082 3083 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3084 3085 :param operation: string "Buy" or "Sell". 3086 :param orderType: string "Limit" or "Stop". 3087 :param lots: volume, integer count of lots >= 1. 3088 :param targetPrice: target price > 0. This is open trade price for limit order. 3089 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3090 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3091 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3092 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3093 Stop loss order always executed by market price. 3094 :param expDate: string "Undefined" by default or local date in future. 3095 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3096 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3097 A limit order has no expiration date, it lasts until the end of the trading day. 3098 :return: JSON with response from broker server. 3099 """ 3100 if self.accountId is None or not self.accountId: 3101 uLogger.error("Variable `accountId` must be defined for using this method!") 3102 raise Exception("Account ID required") 3103 3104 if operation is None or not operation or operation not in ("Buy", "Sell"): 3105 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3106 raise Exception("Incorrect value") 3107 3108 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3109 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3110 raise Exception("Incorrect value") 3111 3112 if lots is None or lots < 1: 3113 uLogger.error("You must define trade volume > 0: integer count of lots!") 3114 raise Exception("Incorrect value") 3115 3116 if targetPrice is None or targetPrice <= 0: 3117 uLogger.error("Target price for limit-order must be greater than 0!") 3118 raise Exception("Incorrect value") 3119 3120 if limitPrice is None or limitPrice <= 0: 3121 limitPrice = targetPrice 3122 3123 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3124 stopType = "Limit" 3125 3126 if expDate is None or not expDate: 3127 expDate = "Undefined" 3128 3129 if not (self.ticker or self.figi): 3130 uLogger.error("Tocker or FIGI must be defined!") 3131 raise Exception("Ticker or FIGI required") 3132 3133 response = {} 3134 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3135 self.ticker = instrument["ticker"] 3136 self.figi = instrument["figi"] 3137 3138 if orderType == "Limit": 3139 uLogger.debug( 3140 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3141 self.ticker, self.figi, 3142 operation, lots, targetPrice, instrument["currency"], 3143 )) 3144 3145 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3146 self.body = str({ 3147 "figi": self.figi, 3148 "quantity": str(lots), 3149 "price": FloatToNano(targetPrice), 3150 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3151 "accountId": str(self.accountId), 3152 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3153 }) 3154 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3155 3156 if "orderId" in response.keys(): 3157 uLogger.info( 3158 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3159 response["orderId"], 3160 self.ticker, self.figi, 3161 operation, lots, targetPrice, instrument["currency"], 3162 )) 3163 3164 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3165 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3166 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3167 targetPrice, instrument["currency"], 3168 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3169 )) 3170 3171 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3172 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3173 targetPrice, instrument["currency"], 3174 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3175 )) 3176 3177 else: 3178 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3179 3180 if orderType == "Stop": 3181 uLogger.debug( 3182 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3183 self.ticker, self.figi, 3184 operation, lots, 3185 targetPrice, instrument["currency"], 3186 limitPrice, instrument["currency"], 3187 stopType, expDate, 3188 )) 3189 3190 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3191 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3192 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3193 3194 body = { 3195 "figi": self.figi, 3196 "quantity": str(lots), 3197 "price": FloatToNano(limitPrice), 3198 "stopPrice": FloatToNano(targetPrice), 3199 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3200 "accountId": str(self.accountId), 3201 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3202 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3203 } 3204 3205 if expDateUTC: 3206 body["expireDate"] = expDateUTC 3207 3208 self.body = str(body) 3209 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3210 3211 if "stopOrderId" in response.keys(): 3212 uLogger.info( 3213 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3214 response["stopOrderId"], 3215 self.ticker, self.figi, 3216 operation, lots, 3217 targetPrice, instrument["currency"], 3218 limitPrice, instrument["currency"], 3219 TKS_STOP_ORDER_TYPES[stopOrderType], 3220 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3221 )) 3222 3223 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3224 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3225 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3226 targetPrice, instrument["currency"], 3227 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3228 )) 3229 3230 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3231 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3232 targetPrice, instrument["currency"], 3233 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3234 )) 3235 3236 else: 3237 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3238 3239 return response 3240 3241 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3242 """ 3243 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3244 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3245 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3246 See also: `Order()` docstring. 3247 3248 :param lots: volume, integer count of lots >= 1. 3249 :param targetPrice: target price > 0. This is open trade price for limit order. 3250 :return: JSON with response from broker server. 3251 """ 3252 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3253 3254 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3255 """ 3256 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3257 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3258 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3259 target price value then broker opens a limit order. See also: `Order()` docstring. 3260 3261 :param lots: volume, integer count of lots >= 1. 3262 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3263 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3264 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3265 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3266 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3267 :param expDate: string "Undefined" by default or local date in future. 3268 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3269 This date is converting to UTC format for server. 3270 :return: JSON with response from broker server. 3271 """ 3272 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3273 3274 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3275 """ 3276 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3277 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3278 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3279 See also: `Order()` docstring. 3280 3281 :param lots: volume, integer count of lots >= 1. 3282 :param targetPrice: target price > 0. This is open trade price for limit order. 3283 :return: JSON with response from broker server. 3284 """ 3285 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3286 3287 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3288 """ 3289 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3290 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3291 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3292 target price value then broker opens a limit order. See also: `Order()` docstring. 3293 3294 :param lots: volume, integer count of lots >= 1. 3295 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3296 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3297 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3298 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3299 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3300 :param expDate: string "Undefined" by default or local date in future. 3301 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3302 This date is converting to UTC format for server. 3303 :return: JSON with response from broker server. 3304 """ 3305 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3306 3307 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3308 """ 3309 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3310 3311 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3312 :param allOrdersIDs: pre-received lists of all active pending orders. 3313 This avoids unnecessary downloading data from the server. 3314 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3315 """ 3316 if self.accountId is None or not self.accountId: 3317 uLogger.error("Variable `accountId` must be defined for using this method!") 3318 raise Exception("Account ID required") 3319 3320 if orderIDs: 3321 if allOrdersIDs is None or not allOrdersIDs: 3322 rawOrders = self.RequestPendingOrders() 3323 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3324 3325 if allStopOrdersIDs is None or not allStopOrdersIDs: 3326 rawStopOrders = self.RequestStopOrders() 3327 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3328 3329 for orderID in orderIDs: 3330 idInPendingOrders = orderID in allOrdersIDs 3331 idInStopOrders = orderID in allStopOrdersIDs 3332 3333 if not (idInPendingOrders or idInStopOrders): 3334 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3335 continue 3336 3337 else: 3338 if idInPendingOrders: 3339 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3340 3341 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3342 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3343 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3344 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3345 3346 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3347 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3348 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3349 3350 else: 3351 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3352 3353 elif idInStopOrders: 3354 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3355 3356 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3357 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3358 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3359 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3360 3361 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3362 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3363 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3364 3365 else: 3366 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3367 3368 else: 3369 continue 3370 3371 def CloseAllOrders(self) -> None: 3372 """ 3373 Gets a list of open pending and stop orders and cancel it all. 3374 """ 3375 rawOrders = self.RequestPendingOrders() 3376 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3377 lenOrders = len(allOrdersIDs) 3378 3379 rawStopOrders = self.RequestStopOrders() 3380 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3381 lenSOrders = len(allStopOrdersIDs) 3382 3383 if lenOrders > 0 or lenSOrders > 0: 3384 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3385 3386 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3387 3388 else: 3389 uLogger.info("Orders not found, nothing to cancel.") 3390 3391 def CloseAll(self, *args) -> None: 3392 """ 3393 Close all available (not blocked) opened trades and orders. 3394 3395 Also, you can select one or more keywords case-insensitive: 3396 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3397 3398 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3399 """ 3400 overview = self.Overview(show=False) # get all open trades info 3401 3402 if len(args) == 0: 3403 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3404 self.CloseAllOrders() # close all pending and stop orders 3405 3406 for iType in TKS_INSTRUMENTS: 3407 if iType != "Currencies": 3408 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3409 3410 else: 3411 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3412 lowerArgs = [x.lower() for x in args] 3413 3414 if "orders" in lowerArgs: 3415 self.CloseAllOrders() # close all pending and stop orders 3416 3417 for iType in TKS_INSTRUMENTS: 3418 if iType.lower() in lowerArgs and iType != "Currencies": 3419 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3420 3421 @staticmethod 3422 def ParseOrderParameters(operation, **inputParameters): 3423 """ 3424 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3425 3426 :param operation: string "Buy" or "Sell". 3427 :param inputParameters: this is dict of strings that looks like this 3428 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3429 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3430 "prices" key: one or more prices to open limit-orders 3431 Counts of values in lots and prices lists must be equals! 3432 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3433 """ 3434 # TODO: update order grid work with api v2 3435 pass 3436 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3437 # 3438 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3439 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3440 # raise Exception("Incorrect value") 3441 # 3442 # if "l" in inputParameters.keys(): 3443 # inputParameters["lots"] = inputParameters.pop("l") 3444 # 3445 # if "p" in inputParameters.keys(): 3446 # inputParameters["prices"] = inputParameters.pop("p") 3447 # 3448 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3449 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3450 # raise Exception("Incorrect value") 3451 # 3452 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3453 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3454 # 3455 # if len(lots) != len(prices): 3456 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3457 # raise Exception("Incorrect value") 3458 # 3459 # uLogger.debug("Extracted parameters for orders:") 3460 # uLogger.debug("lots = {}".format(lots)) 3461 # uLogger.debug("prices = {}".format(prices)) 3462 # 3463 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3464 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3465 # uLogger.debug("Order parameters: {}".format(result)) 3466 # 3467 # return result 3468 3469 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3470 """ 3471 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3472 3473 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3474 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3475 """ 3476 result = False 3477 msg = "Instrument not defined!" 3478 3479 if portfolio is None or not portfolio: 3480 portfolio = self.Overview(show=False) 3481 3482 if self.ticker: 3483 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3484 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3485 3486 for iType in TKS_INSTRUMENTS: 3487 for instrument in portfolio["stat"][iType]: 3488 if instrument["ticker"] == self.ticker: 3489 result = True 3490 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3491 break 3492 3493 elif self.figi: 3494 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3495 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3496 3497 for iType in TKS_INSTRUMENTS: 3498 for instrument in portfolio["stat"][iType]: 3499 if instrument["figi"] == self.figi: 3500 result = True 3501 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3502 break 3503 3504 else: 3505 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3506 3507 uLogger.debug(msg) 3508 3509 return result 3510 3511 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3512 """ 3513 Returns instrument is in the user's portfolio if it presents there. 3514 Instrument must be defined by `ticker` (highly priority) or `figi`. 3515 3516 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3517 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3518 """ 3519 result = None 3520 msg = "Instrument not defined!" 3521 3522 if portfolio is None or not portfolio: 3523 portfolio = self.Overview(show=False) 3524 3525 if self.ticker: 3526 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3527 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3528 3529 for iType in TKS_INSTRUMENTS: 3530 for instrument in portfolio["stat"][iType]: 3531 if instrument["ticker"] == self.ticker: 3532 result = instrument 3533 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3534 break 3535 3536 elif self.figi: 3537 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3538 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3539 3540 for iType in TKS_INSTRUMENTS: 3541 for instrument in portfolio["stat"][iType]: 3542 if instrument["figi"] == self.figi: 3543 result = instrument 3544 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3545 break 3546 3547 else: 3548 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3549 3550 uLogger.debug(msg) 3551 3552 return result 3553 3554 def RequestLimits(self) -> dict: 3555 """ 3556 Method for obtaining the available funds for withdrawal for current `accountId`. 3557 3558 See also: 3559 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3560 - `OverviewLimits()` method 3561 3562 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3563 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3564 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3565 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3566 """ 3567 if self.accountId is None or not self.accountId: 3568 uLogger.error("Variable `accountId` must be defined for using this method!") 3569 raise Exception("Account ID required") 3570 3571 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3572 3573 self.body = str({"accountId": self.accountId}) 3574 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3575 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3576 3577 uLogger.debug("Records about available funds for withdrawal successfully received") 3578 3579 return rawLimits 3580 3581 def OverviewLimits(self, show: bool = False) -> dict: 3582 """ 3583 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3584 3585 See also: `RequestLimits()`. 3586 3587 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3588 :return: dict with raw parsed data from server and some calculated statistics about it. 3589 """ 3590 if self.accountId is None or not self.accountId: 3591 uLogger.error("Variable `accountId` must be defined for using this method!") 3592 raise Exception("Account ID required") 3593 3594 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3595 3596 view = { 3597 "rawLimits": rawLimits, 3598 "limits": { # parsed data for every currency: 3599 "money": { # this is an array of portfolio currency positions 3600 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3601 }, 3602 "blocked": { # this is an array of blocked currency 3603 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3604 }, 3605 "blockedGuarantee": { # this is locked money under collateral for futures 3606 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3607 }, 3608 }, 3609 } 3610 3611 # --- Prepare text table with limits in human-readable format: 3612 if show: 3613 info = [ 3614 "# Withdrawal limits\n\n", 3615 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3616 "* **Account ID:** [{}]\n".format(self.accountId), 3617 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3618 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3619 ] 3620 3621 for curr in view["limits"]["money"].keys(): 3622 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3623 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3624 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3625 3626 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3627 "[{}]".format(curr), 3628 "{:.2f}".format(view["limits"]["money"][curr]), 3629 "{:.2f}".format(availableMoney), 3630 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3631 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3632 ) 3633 3634 if curr == "rub": 3635 info.insert(5, infoStr) # insert at first position in table and after headers 3636 3637 else: 3638 info.append(infoStr) 3639 3640 infoText = "".join(info) 3641 3642 uLogger.info(infoText) 3643 3644 if self.withdrawalLimitsFile: 3645 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3646 fH.write(infoText) 3647 3648 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3649 3650 return view 3651 3652 def RequestAccounts(self) -> dict: 3653 """ 3654 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3655 3656 See also: 3657 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3658 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3659 - `OverviewUserInfo()` method 3660 3661 :return: dict with raw data from server that contains accounts info. Example of dict: 3662 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3663 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3664 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3665 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3666 """ 3667 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3668 3669 self.body = str({}) 3670 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3671 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3672 3673 uLogger.debug("Records about available accounts successfully received") 3674 3675 return rawAccounts 3676 3677 def RequestUserInfo(self) -> dict: 3678 """ 3679 Method for requesting common user's information. 3680 3681 See also: 3682 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3683 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3684 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3685 - `OverviewUserInfo()` method 3686 3687 :return: dict with raw data from server that contains user's information. Example of dict: 3688 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3689 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3690 """ 3691 uLogger.debug("Requesting common user's information. Wait, please...") 3692 3693 self.body = str({}) 3694 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3695 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3696 3697 uLogger.debug("Records about current user successfully received") 3698 3699 return rawUserInfo 3700 3701 def RequestMarginStatus(self, accountId: str = None) -> dict: 3702 """ 3703 Method for requesting margin calculation for defined account ID. 3704 3705 See also: 3706 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3707 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3708 - `OverviewUserInfo()` method 3709 3710 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3711 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3712 Example of responses: 3713 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3714 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3715 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3716 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3717 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3718 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3719 """ 3720 if accountId is None or not accountId: 3721 if self.accountId is None or not self.accountId: 3722 uLogger.error("Variable `accountId` must be defined for using this method!") 3723 raise Exception("Account ID required") 3724 3725 else: 3726 accountId = self.accountId # use `self.accountId` (main ID) by default 3727 3728 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3729 3730 self.body = str({"accountId": accountId}) 3731 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3732 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3733 3734 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3735 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3736 rawMargin = {} 3737 3738 else: 3739 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3740 3741 return rawMargin 3742 3743 def RequestTariffLimits(self) -> dict: 3744 """ 3745 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3746 3747 See also: 3748 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3749 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3750 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3751 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3752 - `OverviewUserInfo()` method 3753 3754 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3755 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3756 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3757 """ 3758 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3759 3760 self.body = str({}) 3761 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3762 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3763 3764 uLogger.debug("Records with limits of current tariff successfully received") 3765 3766 return rawTariffLimits 3767 3768 def RequestBondCoupons(self, iJSON: dict) -> dict: 3769 """ 3770 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3771 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3772 All dates are in UTC timezone. 3773 3774 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3775 Documentation: 3776 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3777 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3778 3779 See also: `ExtendBondsData()`. 3780 3781 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3782 If raw iJSON is not data of bond then server returns an error [400] with message: 3783 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3784 :return: dictionary with bond payment calendar. Response example 3785 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3786 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3787 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3788 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3789 """ 3790 if iJSON["figi"] is None or not iJSON["figi"]: 3791 uLogger.error("FIGI must be defined for using this method!") 3792 raise Exception("FIGI required") 3793 3794 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3795 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3796 3797 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3798 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3799 self.figi, 3800 startDate, 3801 endDate, 3802 )) 3803 3804 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3805 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3806 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3807 3808 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3809 uLogger.warning("Instrument type is not bond!") 3810 3811 else: 3812 uLogger.debug("Records about bond payment calendar successfully received") 3813 3814 return calendar 3815 3816 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3817 """ 3818 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3819 pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, 3820 coupon yields, current yields and some statistics etc. 3821 3822 WARNING! This is too long operation if a lot of bonds requested from broker server. 3823 3824 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3825 3826 :param instruments: list of strings with tickers or FIGIs. 3827 :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3828 for further used by data scientists or stock analytics. 3829 :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. 3830 In XLSX-file and pandas dataframe fields mean: 3831 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3832 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3833 """ 3834 if instruments is None or not instruments: 3835 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3836 raise Exception("Ticker or FIGI required") 3837 3838 if isinstance(instruments, str): 3839 instruments = [instruments] 3840 3841 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3842 3843 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3844 3845 iCount = len(uniqueInstruments) 3846 tooLong = iCount >= 20 3847 if tooLong: 3848 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3849 3850 bonds = None 3851 for i, self.figi in enumerate(uniqueInstruments): 3852 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3853 3854 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3855 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3856 rawBond = self.SearchByFIGI(requestPrice=True) 3857 3858 # Widen raw data with UTC current time (iData["actualDateTime"]): 3859 actualDate = datetime.now(tzutc()) 3860 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3861 3862 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3863 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3864 3865 # Replace some values with human-readable: 3866 iData["nominalCurrency"] = iData["nominal"]["currency"] 3867 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3868 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3869 iData["aciCurrency"] = iData["aciValue"]["currency"] 3870 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3871 iData["issueSize"] = int(iData["issueSize"]) 3872 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3873 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3874 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3875 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3876 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3877 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3878 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3879 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3880 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3881 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3882 3883 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3884 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3885 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3886 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3887 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3888 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3889 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3890 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3891 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3892 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3893 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3894 3895 # Widen raw data with calendar data from `rawCalendar` values: 3896 calendarData = [] 3897 for item in iData["rawCalendar"]["events"]: 3898 calendarData.append({ 3899 "couponDate": item["couponDate"], 3900 "couponNumber": int(item["couponNumber"]), 3901 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3902 "payCurrency": item["payOneBond"]["currency"], 3903 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3904 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3905 "couponStartDate": item["couponStartDate"], 3906 "couponEndDate": item["couponEndDate"], 3907 "couponPeriod": item["couponPeriod"], 3908 }) 3909 3910 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3911 if "maturityDate" not in iData.keys(): 3912 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3913 3914 # Widen raw data with Coupon Rate. 3915 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3916 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3917 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3918 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3919 3920 # Widen raw data with Yield to Maturity (YTM) on current date. 3921 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3922 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3923 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3924 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3925 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3926 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3927 3928 iData["calendar"] = calendarData # adds calendar at the end 3929 3930 # Remove not used data: 3931 iData.pop("uid") 3932 iData.pop("positionUid") 3933 iData.pop("currentPrice") 3934 iData.pop("rawCalendar") 3935 3936 colNames = list(iData.keys()) 3937 if bonds is None: 3938 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3939 3940 else: 3941 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3942 3943 else: 3944 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3945 3946 processed = round(100 * (i + 1) / iCount, 1) 3947 if tooLong and processed % 5 == 0: 3948 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3949 3950 else: 3951 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3952 3953 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3954 3955 # Saving bonds from pandas dataframe to XLSX sheet: 3956 if xlsx and self.bondsXLSXFile: 3957 with pd.ExcelWriter( 3958 path=self.bondsXLSXFile, 3959 date_format=TKS_DATE_FORMAT, 3960 datetime_format=TKS_DATE_TIME_FORMAT, 3961 mode="w", 3962 ) as writer: 3963 bonds.to_excel( 3964 writer, 3965 sheet_name="Extended bonds data", 3966 index=True, 3967 encoding="UTF-8", 3968 freeze_panes=(1, 1), 3969 ) # saving as XLSX-file with freeze first row and column as headers 3970 3971 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3972 3973 return bonds 3974 3975 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3976 """ 3977 Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default. 3978 3979 WARNING! This is too long operation if a lot of bonds requested from broker server. 3980 3981 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3982 3983 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 3984 extended information about bonds: main info, current prices, bond payment calendar, 3985 coupon yields, current yields and some statistics etc. 3986 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3987 :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3988 for further used by data scientists or stock analytics. 3989 :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3990 """ 3991 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3992 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3993 3994 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3995 3996 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3997 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3998 calendar = None 3999 for bond in extBonds.iterrows(): 4000 for item in bond[1]["calendar"]: 4001 cData = { 4002 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4003 "couponDate": item["couponDate"], 4004 "figi": bond[1]["figi"], 4005 "ticker": bond[1]["ticker"], 4006 "name": bond[1]["name"], 4007 "couponNumber": item["couponNumber"], 4008 "payOneBond": item["payOneBond"], 4009 "payCurrency": item["payCurrency"], 4010 "couponType": item["couponType"], 4011 "couponPeriod": item["couponPeriod"], 4012 "fixDate": item["fixDate"], 4013 "couponStartDate": item["couponStartDate"], 4014 "couponEndDate": item["couponEndDate"], 4015 } 4016 4017 if calendar is None: 4018 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4019 4020 else: 4021 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4022 4023 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4024 4025 # Saving calendar from pandas dataframe to XLSX sheet: 4026 if xlsx: 4027 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4028 4029 with pd.ExcelWriter( 4030 path=xlsxCalendarFile, 4031 date_format=TKS_DATE_FORMAT, 4032 datetime_format=TKS_DATE_TIME_FORMAT, 4033 mode="w", 4034 ) as writer: 4035 humanReadable = calendar.copy(deep=True) 4036 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4037 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4038 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4039 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4040 humanReadable.columns = colNames # human-readable column names 4041 4042 humanReadable.to_excel( 4043 writer, 4044 sheet_name="Bond payments calendar", 4045 index=False, 4046 encoding="UTF-8", 4047 freeze_panes=(1, 2), 4048 ) # saving as XLSX-file with freeze first row and column as headers 4049 4050 del humanReadable # release df in memory 4051 4052 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4053 4054 return calendar 4055 4056 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4057 """ 4058 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4059 Also, creates Markdown file with calendar data, `calendar.md` by default. 4060 4061 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4062 4063 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 4064 extended information about bonds: main info, current prices, bond payment calendar, 4065 coupon yields, current yields and some statistics etc. 4066 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4067 :param show: if `True` then also printing bonds payment calendar to the console, 4068 otherwise save to file `calendarFile` only. `False` by default. 4069 :return: multilines text in Markdown format with bonds payment calendar as a table. 4070 """ 4071 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4072 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4073 4074 infoText = "# Bond payments calendar\n\n" 4075 4076 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate pandas dataframe with full calendar data 4077 4078 if not calendar.empty: 4079 splitLine = "| | | | | | | | | |\n" 4080 4081 info = [ 4082 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4083 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4084 ] 4085 4086 newMonth = False 4087 notOneBond = calendar["figi"].nunique() > 1 4088 for i, bond in enumerate(calendar.iterrows()): 4089 if newMonth and notOneBond: 4090 info.append(splitLine) 4091 4092 info.append( 4093 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4094 " √" if bond[1]["paid"] else " —", 4095 bond[1]["couponDate"].split("T")[0], 4096 bond[1]["figi"], 4097 bond[1]["ticker"], 4098 bond[1]["couponNumber"], 4099 "{} {}".format( 4100 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4101 bond[1]["payCurrency"], 4102 ), 4103 bond[1]["couponType"], 4104 bond[1]["couponPeriod"], 4105 bond[1]["fixDate"].split("T")[0], 4106 ) 4107 ) 4108 4109 if i < len(calendar.values) - 1: 4110 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4111 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4112 newMonth = False if curDate.month == nextDate.month else True 4113 4114 else: 4115 newMonth = False 4116 4117 infoText += "".join(info) 4118 4119 if show: 4120 uLogger.info("{}".format(infoText)) 4121 4122 if self.calendarFile is not None: 4123 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4124 fH.write(infoText) 4125 4126 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4127 4128 else: 4129 infoText += "No data\n" 4130 4131 return infoText 4132 4133 def OverviewAccounts(self, show: bool = False) -> dict: 4134 """ 4135 Method for parsing and show simple table with all available user accounts. 4136 4137 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4138 4139 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4140 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4141 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4142 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4143 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4144 "closed": "—", "access": "Full access" }, ...}}` 4145 """ 4146 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4147 4148 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4149 accounts = { 4150 item["id"]: { 4151 "type": TKS_ACCOUNT_TYPES[item["type"]], 4152 "name": item["name"], 4153 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4154 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4155 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4156 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4157 } for item in rawAccounts["accounts"] 4158 } 4159 4160 # Raw and parsed data with some fields replaced in "stat" section: 4161 view = { 4162 "rawAccounts": rawAccounts, 4163 "stat": accounts, 4164 } 4165 4166 # --- Prepare simple text table with only accounts data in human-readable format: 4167 if show: 4168 info = [ 4169 "# User accounts\n\n", 4170 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4171 "| Account ID | Type | Status | Name |\n", 4172 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4173 ] 4174 4175 for account in view["stat"].keys(): 4176 info.extend([ 4177 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4178 account, 4179 view["stat"][account]["type"], 4180 view["stat"][account]["status"], 4181 view["stat"][account]["name"], 4182 ) 4183 ]) 4184 4185 infoText = "".join(info) 4186 4187 uLogger.info(infoText) 4188 4189 if self.userAccountsFile: 4190 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4191 fH.write(infoText) 4192 4193 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4194 4195 return view 4196 4197 def OverviewUserInfo(self, show: bool = False) -> dict: 4198 """ 4199 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4200 4201 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4202 4203 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4204 :return: dict with raw parsed data from server and some calculated statistics about it. 4205 """ 4206 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4207 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4208 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4209 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4210 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4211 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4212 4213 # This is dict with parsed common user data: 4214 userInfo = { 4215 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4216 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4217 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4218 "tariff": rawUserInfo["tariff"], 4219 } 4220 4221 # This is an array of dict with parsed margin statuses for every account IDs: 4222 margins = {} 4223 for accountId in accounts.keys(): 4224 if rawMargins[accountId]: 4225 margins[accountId] = { 4226 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4227 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4228 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4229 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4230 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4231 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4232 } 4233 4234 else: 4235 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4236 4237 unary = {} # unary-connection limits 4238 for item in rawTariffLimits["unaryLimits"]: 4239 if item["limitPerMinute"] in unary.keys(): 4240 unary[item["limitPerMinute"]].extend(item["methods"]) 4241 4242 else: 4243 unary[item["limitPerMinute"]] = item["methods"] 4244 4245 stream = {} # stream-connection limits 4246 for item in rawTariffLimits["streamLimits"]: 4247 if item["limit"] in stream.keys(): 4248 stream[item["limit"]].extend(item["streams"]) 4249 4250 else: 4251 stream[item["limit"]] = item["streams"] 4252 4253 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4254 limits = { 4255 "unary": unary, 4256 "stream": stream, 4257 } 4258 4259 # Raw and parsed data as an output result: 4260 view = { 4261 "rawUserInfo": rawUserInfo, 4262 "rawAccounts": rawAccounts, 4263 "rawMargins": rawMargins, 4264 "rawTariffLimits": rawTariffLimits, 4265 "stat": { 4266 "userInfo": userInfo, 4267 "accounts": accounts, 4268 "margins": margins, 4269 "limits": limits, 4270 }, 4271 } 4272 4273 # --- Prepare text table with user information in human-readable format: 4274 if show: 4275 info = [ 4276 "# Full user information\n\n", 4277 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4278 "## Common information\n\n", 4279 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4280 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4281 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4282 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4283 "\n## User accounts\n\n", 4284 ] 4285 4286 for account in view["stat"]["accounts"].keys(): 4287 info.extend([ 4288 "### ID: [{}]\n\n".format(account), 4289 "| Parameters | Values |\n", 4290 "|----------------------|--------------------------------------------------------------|\n", 4291 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4292 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4293 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4294 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4295 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4296 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4297 ]) 4298 4299 if margins[account]: 4300 info.extend([ 4301 "| Margin status: | Enabled |\n", 4302 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4303 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4304 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4305 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4306 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4307 ]) 4308 4309 else: 4310 info.append("| Margin status: | Disabled |\n\n") 4311 4312 info.extend([ 4313 "\n## Current user tariff limits\n", 4314 "\nSee also:\n", 4315 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4316 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4317 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4318 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4319 "\n### Unary limits\n", 4320 ]) 4321 4322 if unary: 4323 for key, values in sorted(unary.items()): 4324 info.append("\n* Max requests per minute: {}\n".format(key)) 4325 4326 for value in values: 4327 info.append(" - {}\n".format(value)) 4328 4329 else: 4330 info.append("\nNot available\n") 4331 4332 info.append("\n### Stream limits\n") 4333 4334 if stream: 4335 for key, values in sorted(stream.items()): 4336 info.append("\n* Max stream connections: {}\n".format(key)) 4337 4338 for value in values: 4339 info.append(" - {}\n".format(value)) 4340 4341 else: 4342 info.append("\nNot available\n") 4343 4344 infoText = "".join(info) 4345 4346 uLogger.info(infoText) 4347 4348 if self.userInfoFile: 4349 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4350 fH.write(infoText) 4351 4352 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4353 4354 return view 4355 4356 4357class Args: 4358 """ 4359 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4360 """ 4361 def __init__(self, **kwargs): 4362 self.__dict__.update(kwargs) 4363 4364 def __getattr__(self, item): 4365 return None 4366 4367 4368def ParseArgs(): 4369 """ 4370 Function get and parse command line keys. 4371 4372 See examples: 4373 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4374 - in russian: https://tim55667757.github.io/TKSBrokerAPI/ 4375 """ 4376 parser = ArgumentParser() # command-line string parser 4377 4378 parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples" 4379 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4380 4381 # --- options: 4382 4383 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.") 4384 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4385 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4386 4387 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4388 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4389 4390 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4391 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4392 4393 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4394 4395 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4396 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4397 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4398 4399 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4400 4401 # --- commands: 4402 4403 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4404 4405 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4406 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4407 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4408 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4409 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4410 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4411 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4412 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4413 4414 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4415 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4416 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4417 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4418 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4419 4420 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4421 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4422 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4423 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4424 4425 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4426 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4427 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4428 4429 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4430 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4431 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4432 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4433 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4434 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4435 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4436 4437 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4438 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4439 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4440 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4441 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4442 4443 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4444 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4445 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4446 4447 cmdArgs = parser.parse_args() 4448 return cmdArgs 4449 4450 4451def Main(**kwargs): 4452 """ 4453 Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command. 4454 4455 See examples: 4456 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4457 - in russian: https://tim55667757.github.io/TKSBrokerAPI/ 4458 """ 4459 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4460 4461 if args.debug_level: 4462 uLogger.level = 10 # always debug level by default 4463 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4464 4465 exitCode = 0 4466 start = datetime.now(tzutc()) 4467 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4468 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4469 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4470 )) 4471 4472 # trying to calculate full current version: 4473 buildVersion = __version__ 4474 try: 4475 v = version("tksbrokerapi") 4476 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4477 4478 except Exception: 4479 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4480 4481 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4482 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4483 4484 try: 4485 if args.version: 4486 print("TKSBrokerAPI {}".format(buildVersion)) 4487 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4488 4489 else: 4490 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4491 server = TinkoffBrokerServer( 4492 token=args.token, 4493 accountId=args.account_id, 4494 useCache=not args.no_cache, 4495 ) 4496 4497 # --- set some options: 4498 4499 if args.ticker: 4500 if args.ticker in server.aliasesKeys: 4501 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4502 4503 else: 4504 server.ticker = args.ticker 4505 4506 if args.figi: 4507 server.figi = args.figi 4508 4509 if args.depth is not None: 4510 server.depth = args.depth 4511 4512 # --- do one of commands: 4513 4514 if args.list: 4515 if args.output is not None: 4516 server.instrumentsFile = args.output 4517 4518 server.ShowInstrumentsInfo(show=True) 4519 4520 elif args.list_xlsx: 4521 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4522 4523 elif args.bonds_xlsx is not None: 4524 if args.output is not None: 4525 server.bondsXLSXFile = args.output 4526 4527 if len(args.bonds_xlsx) == 0: 4528 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4529 4530 else: 4531 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4532 4533 elif args.search: 4534 if args.output is not None: 4535 server.searchResultsFile = args.output 4536 4537 server.SearchInstruments(pattern=args.search[0], show=True) 4538 4539 elif args.info: 4540 if not (args.ticker or args.figi): 4541 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4542 raise Exception("Ticker or FIGI required") 4543 4544 if args.output is not None: 4545 server.infoFile = args.output 4546 4547 if args.ticker: 4548 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4549 4550 else: 4551 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4552 4553 elif args.calendar is not None: 4554 if args.output is not None: 4555 server.calendarFile = args.output 4556 4557 if len(args.calendar) == 0: 4558 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4559 4560 else: 4561 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4562 4563 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4564 4565 elif args.price: 4566 if not (args.ticker or args.figi): 4567 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4568 raise Exception("Ticker or FIGI required") 4569 4570 server.GetCurrentPrices(show=True) 4571 4572 elif args.prices is not None: 4573 if args.output is not None: 4574 server.pricesFile = args.output 4575 4576 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4577 4578 elif args.overview: 4579 if args.output is not None: 4580 server.overviewFile = args.output 4581 4582 server.Overview(show=True, details="full") 4583 4584 elif args.overview_digest: 4585 if args.output is not None: 4586 server.overviewDigestFile = args.output 4587 4588 server.Overview(show=True, details="digest") 4589 4590 elif args.overview_positions: 4591 if args.output is not None: 4592 server.overviewPositionsFile = args.output 4593 4594 server.Overview(show=True, details="positions") 4595 4596 elif args.overview_orders: 4597 if args.output is not None: 4598 server.overviewOrdersFile = args.output 4599 4600 server.Overview(show=True, details="orders") 4601 4602 elif args.overview_analytics: 4603 if args.output is not None: 4604 server.overviewAnalyticsFile = args.output 4605 4606 server.Overview(show=True, details="analytics") 4607 4608 elif args.deals is not None: 4609 if args.output is not None: 4610 server.reportFile = args.output 4611 4612 if 0 <= len(args.deals) < 3: 4613 server.Deals( 4614 start=args.deals[0] if len(args.deals) >= 1 else None, 4615 end=args.deals[1] if len(args.deals) == 2 else None, 4616 show=True, # Always show deals report in console 4617 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4618 ) 4619 4620 else: 4621 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4622 raise Exception("Incorrect value") 4623 4624 elif args.history is not None: 4625 if args.output is not None: 4626 server.historyFile = args.output 4627 4628 if 0 <= len(args.history) < 3: 4629 dataReceived = server.History( 4630 start=args.history[0] if len(args.history) >= 1 else None, 4631 end=args.history[1] if len(args.history) == 2 else None, 4632 interval="hour" if args.interval is None or not args.interval else args.interval, 4633 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4634 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4635 show=True, # shows all downloaded candles in console 4636 ) 4637 4638 if args.render_chart is not None and dataReceived is not None: 4639 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4640 4641 server.ShowHistoryChart( 4642 candles=dataReceived, 4643 interact=iChart, 4644 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4645 ) 4646 4647 else: 4648 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4649 raise Exception("Incorrect value") 4650 4651 elif args.load_history is not None: 4652 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4653 4654 if args.render_chart is not None and histData is not None: 4655 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4656 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4657 4658 server.ShowHistoryChart( 4659 candles=histData, 4660 interact=iChart, 4661 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4662 ) 4663 4664 elif args.trade is not None: 4665 if 1 <= len(args.trade) <= 5: 4666 server.Trade( 4667 operation=args.trade[0], 4668 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4669 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4670 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4671 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4672 ) 4673 4674 else: 4675 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4676 4677 elif args.buy is not None: 4678 if 0 <= len(args.buy) <= 4: 4679 server.Buy( 4680 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4681 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4682 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4683 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4684 ) 4685 4686 else: 4687 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4688 4689 elif args.sell is not None: 4690 if 0 <= len(args.sell) <= 4: 4691 server.Sell( 4692 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4693 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4694 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4695 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4696 ) 4697 4698 else: 4699 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4700 4701 elif args.order: 4702 if 4 <= len(args.order) <= 7: 4703 server.Order( 4704 operation=args.order[0], 4705 orderType=args.order[1], 4706 lots=int(args.order[2]), 4707 targetPrice=float(args.order[3]), 4708 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4709 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4710 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4711 ) 4712 4713 else: 4714 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4715 4716 elif args.buy_limit: 4717 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4718 4719 elif args.sell_limit: 4720 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4721 4722 elif args.buy_stop: 4723 if 2 <= len(args.buy_stop) <= 7: 4724 server.BuyStop( 4725 lots=int(args.buy_stop[0]), 4726 targetPrice=float(args.buy_stop[1]), 4727 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4728 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4729 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4730 ) 4731 4732 else: 4733 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4734 4735 elif args.sell_stop: 4736 if 2 <= len(args.sell_stop) <= 7: 4737 server.SellStop( 4738 lots=int(args.sell_stop[0]), 4739 targetPrice=float(args.sell_stop[1]), 4740 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4741 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4742 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4743 ) 4744 4745 else: 4746 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4747 4748 # elif args.buy_order_grid is not None: 4749 # # update order grid work with api v2 4750 # if len(args.buy_order_grid) == 2: 4751 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4752 # 4753 # for order in orderParams: 4754 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4755 # 4756 # else: 4757 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4758 # 4759 # elif args.sell_order_grid is not None: 4760 # # update order grid work with api v2 4761 # if len(args.sell_order_grid) >= 2: 4762 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4763 # 4764 # for order in orderParams: 4765 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4766 # 4767 # else: 4768 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4769 4770 elif args.close_order is not None: 4771 server.CloseOrders(args.close_order) # close only one order 4772 4773 elif args.close_orders is not None: 4774 server.CloseOrders(args.close_orders) # close list of orders 4775 4776 elif args.close_trade: 4777 if not args.ticker: 4778 uLogger.error("`--ticker` key is required for this operation!") 4779 raise Exception("Ticker required") 4780 4781 server.CloseTrades([args.ticker]) # close only one trade 4782 4783 elif args.close_trades is not None: 4784 server.CloseTrades(args.close_trades) # close trades for list of tickers 4785 4786 elif args.close_all is not None: 4787 server.CloseAll(*args.close_all) 4788 4789 elif args.limits: 4790 if args.output is not None: 4791 server.withdrawalLimitsFile = args.output 4792 4793 server.OverviewLimits(show=True) 4794 4795 elif args.user_info: 4796 if args.output is not None: 4797 server.userInfoFile = args.output 4798 4799 server.OverviewUserInfo(show=True) 4800 4801 elif args.account: 4802 if args.output is not None: 4803 server.userAccountsFile = args.output 4804 4805 server.OverviewAccounts(show=True) 4806 4807 else: 4808 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4809 raise Exception("There is no command to execute") 4810 4811 except Exception: 4812 trace = tb.format_exc() 4813 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4814 if e in trace: 4815 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4816 break 4817 4818 uLogger.debug(trace) 4819 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4820 exitCode = 255 # an error occurred, must be open a ticket for this issue 4821 4822 finally: 4823 finish = datetime.now(tzutc()) 4824 4825 if exitCode == 0: 4826 uLogger.debug("All operations were finished success (summary code is 0).") 4827 4828 else: 4829 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4830 os.path.abspath(uLog.defaultLogFile), exitCode, 4831 )) 4832 4833 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4834 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4835 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4836 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4837 )) 4838 4839 if not kwargs: 4840 sys.exit(exitCode) 4841 4842 else: 4843 return exitCode 4844 4845 4846if __name__ == "__main__": 4847 Main()
78def NanoToFloat(units: str, nano: int) -> float: 79 """ 80 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 81 82 `NanoToFloat(units="2", nano=500000000) -> 2.5` 83 84 `NanoToFloat(units="0", nano=50000000) -> 0.05` 85 86 :param units: integer string or integer parameter that represents the integer part of number 87 :param nano: integer string or integer parameter that represents the fractional part of number 88 :return: float view of number 89 """ 90 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
93def FloatToNano(number: float) -> dict: 94 """ 95 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 96 97 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 98 99 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 100 101 :param number: float number 102 :return: nano-type view of number: `{"units": "string", "nano": integer}` 103 """ 104 splitByPoint = str(number).split(".") 105 frac = 0 106 107 if len(splitByPoint) > 1: 108 if len(splitByPoint[1]) <= 9: 109 frac = int("{}{}".format( 110 int(splitByPoint[1]), 111 "0" * (9 - len(splitByPoint[1])), 112 )) 113 114 if (number < 0) and (frac > 0): 115 frac = -frac 116 117 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
120def GetDatesAsString(start: str = None, end: str = None) -> tuple: 121 """ 122 Create tuple of date and time strings with timezone parsed from user-friendly date. 123 124 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 125 126 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 127 An error exception will occur if input date has incorrect format. 128 129 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 130 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 131 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 132 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 133 134 Also, you can use keywords for start if `end=None`: 135 `today` (from 00:00:00 to the end of current day), 136 `yesterday` (-1 day from 00:00:00 to 23:59:59), 137 `week` (-7 day from 00:00:00 to the end of current day), 138 `month` (-30 day from 00:00:00 to the end of current day), 139 `year` (-365 day from 00:00:00 to the end of current day), 140 141 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 142 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 143 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 144 """ 145 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 146 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 147 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 148 149 # time between start and the end of the current day: 150 if start is None or start.lower() == "today": 151 pass 152 153 # from start of the last day to the end of the last day: 154 elif start.lower() == "yesterday": 155 s -= timedelta(days=1) 156 e -= timedelta(days=1) 157 158 # week (-7 day from 00:00:00 to the end of the current day): 159 elif start.lower() == "week": 160 s -= timedelta(days=6) # +1 current day already taken into account 161 162 # month (-30 day from 00:00:00 to the end of current day): 163 elif start.lower() == "month": 164 s -= timedelta(days=29) # +1 current day already taken into account 165 166 # year (-365 day from 00:00:00 to the end of current day): 167 elif start.lower() == "year": 168 s -= timedelta(days=364) # +1 current day already taken into account 169 170 # -N days ago to the end of current day: 171 elif start.startswith('-') and start[1:].isdigit(): 172 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 173 174 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 175 else: 176 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 177 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 178 179 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 180 s = s.strftime(TKS_DATE_TIME_FORMAT) 181 e = e.strftime(TKS_DATE_TIME_FORMAT) 182 183 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 184 185 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 - how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
188class TinkoffBrokerServer: 189 """ 190 This class implements methods to work with Tinkoff broker server. 191 192 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 193 194 About `token`: https://tinkoff.github.io/investAPI/token/ 195 """ 196 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 197 """ 198 Main class init. 199 200 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 201 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 202 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 203 :param useCache: use default cache file with raw data to use instead of `iList`. 204 True by default. Cache is auto-update if new day has come. 205 If you don't want to use cache and always updates raw data then set `useCache=False`. 206 :param defaultCache: path to default cache file. `dump.json` by default. 207 """ 208 if token is None or not token: 209 try: 210 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 211 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 212 213 except KeyError: 214 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 215 raise Exception("Token required") 216 217 else: 218 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 219 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 220 221 if accountId is None or not accountId: 222 try: 223 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 224 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 225 226 except KeyError: 227 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 228 229 else: 230 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 231 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 232 233 self.version = __version__ # duplicate here used TKSBrokerAPI main version 234 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 235 236 Latest version: https://pypi.org/project/tksbrokerapi/ 237 """ 238 239 self.aliases = TKS_TICKER_ALIASES 240 """Some aliases instead official tickers. 241 242 See also: `TKSEnums.TKS_TICKER_ALIASES` 243 """ 244 245 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 246 247 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 248 249 self.ticker = "" 250 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 251 252 See also: `SearchByTicker()`, `SearchInstruments()`. 253 """ 254 255 self.figi = "" 256 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 257 258 See also: `SearchByFIGI()`, `SearchInstruments()`. 259 """ 260 261 self.depth = 1 262 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 263 264 See also: `GetCurrentPrices()`. 265 """ 266 267 self.server = r"https://invest-public-api.tinkoff.ru/rest" 268 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 269 270 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 271 """ 272 273 uLogger.debug("Broker API server: {}".format(self.server)) 274 275 self.timeout = 15 276 """Server operations timeout in seconds. Default: `15`. 277 278 See also: `SendAPIRequest()`. 279 """ 280 281 self.headers = { 282 "Content-Type": "application/json", 283 "accept": "application/json", 284 "Authorization": "Bearer {}".format(self.token), 285 "x-app-name": "Tim55667757.TKSBrokerAPI", 286 } 287 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 288 289 See also: `SendAPIRequest()`. 290 """ 291 292 self.body = None 293 """Request body which send to broker server. Default: `None`. 294 295 See also: `SendAPIRequest()`. 296 """ 297 298 self.historyFile = None 299 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe. 300 301 See also: `History()`. 302 """ 303 304 self.htmlHistoryFile = "index.html" 305 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 306 307 See also: `ShowHistoryChart()`. 308 """ 309 310 self.instrumentsFile = "instruments.md" 311 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 312 313 See also: `ShowInstrumentsInfo()`. 314 """ 315 316 self.searchResultsFile = "search-results.md" 317 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 318 319 See also: `SearchInstruments()`. 320 """ 321 322 self.pricesFile = "prices.md" 323 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 324 325 See also: `GetListOfPrices()`. 326 """ 327 328 self.infoFile = "info.md" 329 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 330 331 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 332 """ 333 334 self.bondsXLSXFile = "ext-bonds.xlsx" 335 """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 336 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 337 338 See also: `ExtendBondsData()`. 339 """ 340 341 self.calendarFile = "calendar.md" 342 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 343 344 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 345 346 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 347 """ 348 349 self.overviewFile = "overview.md" 350 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 351 352 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 353 """ 354 355 self.overviewDigestFile = "overview-digest.md" 356 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 357 358 See also: `Overview()` with parameter `details="digest"`. 359 """ 360 361 self.overviewPositionsFile = "overview-positions.md" 362 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 363 364 See also: `Overview()` with parameter `details="positions"`. 365 """ 366 367 self.overviewOrdersFile = "overview-orders.md" 368 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 369 370 See also: `Overview()` with parameter `details="orders"`. 371 """ 372 373 self.overviewAnalyticsFile = "overview-analytics.md" 374 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 375 376 See also: `Overview()` with parameter `details="analytics"`. 377 """ 378 379 self.reportFile = "deals.md" 380 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 381 382 See also: `Deals()`. 383 """ 384 385 self.withdrawalLimitsFile = "limits.md" 386 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 387 388 See also: `OverviewLimits()` and `RequestLimits()`. 389 """ 390 391 self.userInfoFile = "user-info.md" 392 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 393 394 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 395 """ 396 397 self.userAccountsFile = "accounts.md" 398 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 399 400 See also: `OverviewAccounts()`, `RequestAccounts()`. 401 """ 402 403 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 404 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 405 406 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 407 408 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 409 """ 410 411 self.iList = None # init iList for raw instruments data 412 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 413 414 See also: `Listing()`, `DumpInstruments()`. 415 """ 416 417 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 418 if useCache: 419 if os.path.exists(self.iListDumpFile): 420 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 421 curTime = datetime.now(tzutc()) 422 423 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 424 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 425 426 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 427 428 else: 429 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 430 431 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 432 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 433 434 else: 435 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 436 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 437 438 else: 439 self.iList = self.Listing() # request new raw instruments data from broker server 440 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 441 442 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 443 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 444 445 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 446 """ 447 448 @staticmethod 449 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 450 """ 451 Parse JSON from response string. 452 453 :param rawData: this is a string with JSON-formatted text. 454 :param debug: if `True` then print more debug information. 455 :return: JSON (dictionary), parsed from server response string. 456 """ 457 if debug: 458 uLogger.debug("Raw text body:") 459 uLogger.debug(rawData) 460 461 responseJSON = json.loads(rawData) if rawData else {} 462 463 if debug: 464 uLogger.debug("JSON formatted:") 465 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 466 uLogger.debug(jsonLine) 467 468 return responseJSON 469 470 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 471 """ 472 Send GET or POST request to broker server and receive JSON object. 473 474 self.header: must be defining with dictionary of headers. 475 self.body: if define then used as request body. None by default. 476 self.timeout: global request timeout, 15 seconds by default. 477 :param url: url with REST request. 478 :param reqType: send "GET" or "POST" request. "GET" by default. 479 :param retry: how many times retry after first request if an 5xx server errors occurred. 480 :param pause: sleep time in seconds between retries. 481 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 482 :return: response JSON (dictionary) from broker. 483 """ 484 if reqType not in ("GET", "POST"): 485 uLogger.error("You can define request type: 'GET' or 'POST'!") 486 raise Exception("Incorrect value") 487 488 if debug: 489 uLogger.debug("Request parameters:") 490 uLogger.debug(" - REST API URL: {}".format(url)) 491 uLogger.debug(" - request type: {}".format(reqType)) 492 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 493 uLogger.debug(" - body: {}".format(self.body)) 494 495 # fast hack to avoid all operations with some tickers/FIGI 496 responseJSON = {} 497 oK = True 498 for item in self.exclude: 499 if item in url: 500 if debug: 501 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 502 503 oK = False 504 break 505 506 if oK: 507 counter = 0 508 response = None 509 errMsg = "" 510 511 while not response and counter <= retry: 512 if reqType == "GET": 513 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 514 515 if reqType == "POST": 516 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 517 518 if debug: 519 uLogger.debug("Response:") 520 uLogger.debug(" - status code: {}".format(response.status_code)) 521 uLogger.debug(" - reason: {}".format(response.reason)) 522 uLogger.debug(" - body length: {}".format(len(response.text))) 523 uLogger.debug(" - headers: {}".format(response.headers)) 524 525 # Server returns some headers: 526 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 527 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 528 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 529 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 530 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 531 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 532 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 533 sleep(rateLimitWait) 534 535 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 536 if 400 <= response.status_code < 500: 537 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 538 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 539 counter = retry + 1 540 541 if 500 <= response.status_code < 600: 542 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 543 uLogger.debug(" - not oK, {}".format(errMsg)) 544 counter += 1 545 546 if counter <= retry: 547 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 548 sleep(pause) 549 550 responseJSON = self._ParseJSON(response.text) 551 552 if errMsg: 553 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 554 uLogger.error(" - not oK, {}".format(errMsg)) 555 556 return responseJSON 557 558 def _IUpdater(self, iType: str) -> tuple: 559 """ 560 Request instrument by type from server. See available API methods for instruments: 561 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 562 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 563 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 564 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 565 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 566 567 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 568 :return: tuple with iType name and list of available instruments of current type for defined user token. 569 """ 570 result = [] 571 572 if iType in TKS_INSTRUMENTS: 573 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 574 575 # all instruments have the same body in API v2 requests: 576 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 577 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 578 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 579 580 return iType, result 581 582 def _IWrapper(self, kwargs): 583 """ 584 Wrapper runs instrument's update method `_IUpdater()`. 585 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 586 """ 587 return self._IUpdater(**kwargs) 588 589 def Listing(self) -> dict: 590 """ 591 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 592 593 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 594 """ 595 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 596 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 597 598 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 599 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 600 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 601 602 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 603 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 604 poolUpdater.close() 605 606 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 607 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 608 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 609 610 # calculate minimum price increment (step) for all instruments and set up instrument's type: 611 for iType in iList.keys(): 612 for ticker in iList[iType]: 613 iList[iType][ticker]["type"] = iType 614 615 if "minPriceIncrement" in iList[iType][ticker].keys(): 616 iList[iType][ticker]["step"] = NanoToFloat( 617 iList[iType][ticker]["minPriceIncrement"]["units"], 618 iList[iType][ticker]["minPriceIncrement"]["nano"], 619 ) 620 621 else: 622 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 623 624 return iList 625 626 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 627 """ 628 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 629 630 See also: `DumpInstruments()`, `Listing()`. 631 632 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 633 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 634 """ 635 if self.iListDumpFile is None or not self.iListDumpFile: 636 uLogger.error("Output name of dump file must be defined!") 637 raise Exception("Filename required") 638 639 if not self.iList or forceUpdate: 640 self.iList = self.Listing() 641 642 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 643 644 # Save as XLSX with separated sheets for every type of instruments: 645 with pd.ExcelWriter( 646 path=xlsxDumpFile, 647 date_format=TKS_DATE_FORMAT, 648 datetime_format=TKS_DATE_TIME_FORMAT, 649 mode="w", 650 ) as writer: 651 for iType in TKS_INSTRUMENTS: 652 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 653 df = df[sorted(df)] # sorted by column names 654 df = df.applymap( 655 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 656 na_action="ignore", 657 ) # converting numbers from nano-type to float in every cell 658 df.to_excel( 659 writer, 660 sheet_name=iType, 661 encoding="UTF-8", 662 freeze_panes=(1, 1), 663 ) # saving as XLSX-file with freeze first row and column as headers 664 665 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 666 667 def DumpInstruments(self, forceUpdate: bool = True) -> str: 668 """ 669 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 670 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 671 672 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 673 674 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 675 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 676 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 677 """ 678 if self.iListDumpFile is None or not self.iListDumpFile: 679 uLogger.error("Output name of dump file must be defined!") 680 raise Exception("Filename required") 681 682 if not self.iList or forceUpdate: 683 self.iList = self.Listing() 684 685 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 686 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 687 fH.write(jsonDump) 688 689 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 690 691 return jsonDump 692 693 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 694 """ 695 Show information about one instrument defined by json data and prints it in Markdown format. 696 697 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 698 699 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 700 :param show: if `True` then also printing information about instrument and its current price. 701 :return: multilines text in Markdown format with information about one instrument. 702 """ 703 splitLine = "| | |\n" 704 infoText = "" 705 706 if iJSON is not None and iJSON and isinstance(iJSON, dict): 707 info = [ 708 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 709 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 710 "| Parameters | Values |\n", 711 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 712 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 713 "| Full name: | {:<54} |\n".format(iJSON["name"]), 714 ] 715 716 if "sector" in iJSON.keys() and iJSON["sector"]: 717 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 718 719 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 720 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 721 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 722 ))) 723 724 info.extend([ 725 splitLine, 726 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 727 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 728 ]) 729 730 if "isin" in iJSON.keys() and iJSON["isin"]: 731 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 732 733 if "classCode" in iJSON.keys(): 734 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 735 736 info.extend([ 737 splitLine, 738 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 739 splitLine, 740 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 741 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 742 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 743 ]) 744 745 if iJSON["figi"]: 746 self.figi = iJSON["figi"] 747 iJSON = iJSON | self.RequestTradingStatus() 748 749 info.extend([ 750 splitLine, 751 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 752 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 753 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 754 ]) 755 756 info.append(splitLine) 757 758 if "type" in iJSON.keys() and iJSON["type"]: 759 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 760 761 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 762 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 763 764 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 765 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 766 767 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 768 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 769 770 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 771 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 772 773 if "focusType" in iJSON.keys() and iJSON["focusType"]: 774 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 775 776 if "assetType" in iJSON.keys() and iJSON["assetType"]: 777 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 778 779 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 780 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 781 782 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 783 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 784 785 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 786 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 787 788 if "currency" in iJSON.keys(): 789 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 790 791 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 792 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 793 794 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 795 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 796 797 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 798 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 799 800 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 801 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 802 803 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 804 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 805 806 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 807 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 808 809 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 810 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 811 812 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 813 info.append("| Perpetual bond: | Yes |\n") 814 815 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 816 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 817 818 iExt = None 819 if iJSON["type"] == "Bonds": 820 info.extend([ 821 splitLine, 822 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 823 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 824 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 825 iJSON["nominal"]["currency"], 826 )), 827 ]) 828 829 if "floatingCouponFlag" in iJSON.keys(): 830 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 831 832 if "amortizationFlag" in iJSON.keys(): 833 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 834 835 info.append(splitLine) 836 837 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 838 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 839 840 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 841 842 info.extend([ 843 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 844 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 845 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 846 ]) 847 848 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 849 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 850 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 851 iJSON["aciValue"]["currency"] 852 ))) 853 854 if "currentPrice" in iJSON.keys(): 855 info.append(splitLine) 856 857 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 858 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 859 860 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 861 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 862 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 863 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 864 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 865 866 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 867 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 868 869 info.extend([ 870 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 871 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 872 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 873 )), 874 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 875 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 876 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 877 )), 878 "| Changes between last deal price and last close | {:<54} |\n".format( 879 "{:.2f}%{}".format( 880 iJSON["currentPrice"]["changes"], 881 " ({}{:.2f} {})".format( 882 "+" if bondChangesDelta > 0 else "", 883 bondChangesDelta, 884 aciCurrency 885 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 886 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 887 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 888 currency 889 ), 890 ) 891 ), 892 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 893 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 894 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 895 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 898 )), 899 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 900 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 901 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 902 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 904 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 905 )), 906 ]) 907 908 if "lot" in iJSON.keys(): 909 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 910 911 if "step" in iJSON.keys() and iJSON["step"] != 0: 912 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 913 914 # Add bond payment calendar: 915 if iJSON["type"] == "Bonds": 916 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 917 info.extend(["\n", strCalendar]) 918 919 infoText += "".join(info) 920 921 if show: 922 uLogger.info("{}".format(infoText)) 923 924 else: 925 uLogger.debug("{}".format(infoText)) 926 927 if self.infoFile is not None: 928 with open(self.infoFile, "w", encoding="UTF-8") as fH: 929 fH.write(infoText) 930 931 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 932 933 return infoText 934 935 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 936 """ 937 Search and return raw broker's information about instrument by its ticker. 938 `ticker` must be defined! If debug=True then print all debug messages. 939 940 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 941 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 942 :param debug: if `True` then print all debug console messages. 943 :return: JSON formatted data with information about instrument. 944 """ 945 tickerJSON = {} 946 if debug: 947 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 948 949 if not self.ticker: 950 uLogger.warning("self.ticker variable is not be empty!") 951 952 else: 953 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 954 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 955 raise Exception("Instrument not allowed") 956 957 if not self.iList: 958 self.iList = self.Listing() 959 960 if self.ticker in self.iList["Shares"].keys(): 961 tickerJSON = self.iList["Shares"][self.ticker] 962 if debug: 963 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 964 965 elif self.ticker in self.iList["Currencies"].keys(): 966 tickerJSON = self.iList["Currencies"][self.ticker] 967 if debug: 968 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 969 970 elif self.ticker in self.iList["Bonds"].keys(): 971 tickerJSON = self.iList["Bonds"][self.ticker] 972 if debug: 973 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 974 975 elif self.ticker in self.iList["Etfs"].keys(): 976 tickerJSON = self.iList["Etfs"][self.ticker] 977 if debug: 978 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 979 980 elif self.ticker in self.iList["Futures"].keys(): 981 tickerJSON = self.iList["Futures"][self.ticker] 982 if debug: 983 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 984 985 if tickerJSON: 986 self.figi = tickerJSON["figi"] 987 988 if requestPrice: 989 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 990 991 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 992 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 993 994 else: 995 tickerJSON["currentPrice"]["changes"] = 0 996 997 if show: 998 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 999 1000 else: 1001 if show: 1002 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1003 1004 return tickerJSON 1005 1006 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1007 """ 1008 Search and return raw broker's information about instrument by its FIGI. 1009 `figi` must be defined! If debug=True then print all debug messages. 1010 1011 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1012 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1013 :param debug: if `True` then print all debug console messages. 1014 :return: JSON formatted data with information about instrument. 1015 """ 1016 figiJSON = {} 1017 if debug: 1018 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1019 1020 if not self.figi: 1021 uLogger.warning("self.figi variable is not be empty!") 1022 1023 else: 1024 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1025 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1026 raise Exception("Instrument not allowed") 1027 1028 if not self.iList: 1029 self.iList = self.Listing() 1030 1031 for item in self.iList["Shares"].keys(): 1032 if self.figi == self.iList["Shares"][item]["figi"]: 1033 figiJSON = self.iList["Shares"][item] 1034 1035 if debug: 1036 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1037 1038 break 1039 1040 if not figiJSON: 1041 for item in self.iList["Currencies"].keys(): 1042 if self.figi == self.iList["Currencies"][item]["figi"]: 1043 figiJSON = self.iList["Currencies"][item] 1044 1045 if debug: 1046 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1047 1048 break 1049 1050 if not figiJSON: 1051 for item in self.iList["Bonds"].keys(): 1052 if self.figi == self.iList["Bonds"][item]["figi"]: 1053 figiJSON = self.iList["Bonds"][item] 1054 1055 if debug: 1056 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1057 1058 break 1059 1060 if not figiJSON: 1061 for item in self.iList["Etfs"].keys(): 1062 if self.figi == self.iList["Etfs"][item]["figi"]: 1063 figiJSON = self.iList["Etfs"][item] 1064 1065 if debug: 1066 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1067 1068 break 1069 1070 if not figiJSON: 1071 for item in self.iList["Futures"].keys(): 1072 if self.figi == self.iList["Futures"][item]["figi"]: 1073 figiJSON = self.iList["Futures"][item] 1074 1075 if debug: 1076 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1077 1078 break 1079 1080 if figiJSON: 1081 self.figi = figiJSON["figi"] 1082 self.ticker = figiJSON["ticker"] 1083 1084 if requestPrice: 1085 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1086 1087 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1088 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1089 1090 else: 1091 figiJSON["currentPrice"]["changes"] = 0 1092 1093 if show: 1094 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1095 1096 else: 1097 if show: 1098 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1099 1100 return figiJSON 1101 1102 def GetCurrentPrices(self, show: bool = True) -> dict: 1103 """ 1104 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1105 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1106 1107 See also: 1108 1109 :param show: if `True` then print DOM to log and console. 1110 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1111 """ 1112 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1113 1114 if self.depth < 1: 1115 uLogger.error("Depth of Market (DOM) must be >=1!") 1116 raise Exception("Incorrect value") 1117 1118 if not (self.ticker or self.figi): 1119 uLogger.error("self.ticker or self.figi variables must be defined!") 1120 raise Exception("Ticker or FIGI required") 1121 1122 if self.ticker and not self.figi: 1123 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1124 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1125 1126 if not self.ticker and self.figi: 1127 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1128 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1129 1130 if not self.figi: 1131 uLogger.error("FIGI is not defined!") 1132 raise Exception("Ticker or FIGI required") 1133 1134 else: 1135 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1136 1137 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1138 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1139 self.body = str({"figi": self.figi, "depth": self.depth}) 1140 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1141 1142 if pricesResponse: 1143 # list of dicts with sellers orders: 1144 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1145 1146 # list of dicts with buyers orders: 1147 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1148 1149 # max price of instrument at this time: 1150 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1151 1152 # min price of instrument at this time: 1153 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1154 1155 # last price of deal with instrument: 1156 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1157 1158 # last close price of instrument: 1159 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1160 1161 else: 1162 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1163 uLogger.debug("Server response: {}".format(pricesResponse)) 1164 1165 if show: 1166 if prices["buy"] or prices["sell"]: 1167 info = [ 1168 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1169 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1170 self.ticker, 1171 self.figi, 1172 self.depth, 1173 ), 1174 uLog.sepShort, "\n", 1175 " Orders of Buyers | Orders of Sellers\n", 1176 uLog.sepShort, "\n", 1177 " Sell prices (vol.) | Buy prices (vol.)\n", 1178 uLog.sepShort, "\n", 1179 ] 1180 1181 if not prices["buy"]: 1182 info.append(" | No orders!\n") 1183 sumBuy = 0 1184 1185 else: 1186 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1187 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1188 for item in maxMinSorted: 1189 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1190 1191 if not prices["sell"]: 1192 info.append("No orders! |\n") 1193 sumSell = 0 1194 1195 else: 1196 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1197 for item in prices["sell"]: 1198 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1199 1200 info.extend([ 1201 uLog.sepShort, "\n", 1202 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1203 uLog.sepShort, "\n", 1204 ]) 1205 1206 infoText = "".join(info) 1207 1208 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1209 1210 else: 1211 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1212 1213 return prices 1214 1215 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1216 """ 1217 This method get and show information about all available broker instruments for current user account. 1218 If `instrumentsFile` string is not empty then also save information to this file. 1219 1220 :param show: if `True` then print results to console, if `False` - print only to file. 1221 :return: multi-lines string with all available broker instruments 1222 """ 1223 if not self.iList: 1224 self.iList = self.Listing() 1225 1226 info = [ 1227 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1228 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1229 ] 1230 1231 # add instruments count by type: 1232 for iType in self.iList.keys(): 1233 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1234 1235 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1236 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1237 1238 # generating info tables with all instruments by type: 1239 for iType in self.iList.keys(): 1240 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1241 1242 for instrument in self.iList[iType].keys(): 1243 iName = self.iList[iType][instrument]["name"] # instrument's name 1244 if len(iName) > 57: 1245 iName = "{}...".format(iName[:54]) # right trim for a long string 1246 1247 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1248 self.iList[iType][instrument]["ticker"], 1249 iName, 1250 self.iList[iType][instrument]["figi"], 1251 self.iList[iType][instrument]["currency"], 1252 self.iList[iType][instrument]["lot"], 1253 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1254 )) 1255 1256 infoText = "".join(info) 1257 1258 if show: 1259 uLogger.info(infoText) 1260 1261 if self.instrumentsFile: 1262 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1263 fH.write(infoText) 1264 1265 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1266 1267 return infoText 1268 1269 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1270 """ 1271 This method search and show information about instruments by part of its ticker, FIGI or name. 1272 If `searchResultsFile` string is not empty then also save information to this file. 1273 1274 :param pattern: string with part of ticker, FIGI or instrument's name. 1275 :param show: if `True` then print results to console, if `False` - return list of result only. 1276 :return: list of dictionaries with all found instruments. 1277 """ 1278 if not self.iList: 1279 self.iList = self.Listing() 1280 1281 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1282 compiledPattern = re.compile(pattern, re.IGNORECASE) 1283 1284 for iType in self.iList: 1285 for instrument in self.iList[iType].values(): 1286 searchResult = compiledPattern.search(" ".join( 1287 [instrument["ticker"], instrument["figi"], instrument["name"]] 1288 )) 1289 1290 if searchResult: 1291 searchResults[iType][instrument["ticker"]] = instrument 1292 1293 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1294 info = [ 1295 "# Search results\n\n", 1296 "* **Search pattern:** [{}]\n".format(pattern), 1297 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1298 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1299 ] 1300 infoShort = info[:] 1301 1302 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1303 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1304 skippedLine = "| ... | ... | ... | ... |\n" 1305 1306 if resultsLen == 0: 1307 info.append("\nNo results\n") 1308 infoShort.append("\nNo results\n") 1309 uLogger.warning("No results. Try changing your search pattern.") 1310 1311 else: 1312 for iType in searchResults: 1313 iTypeValuesCount = len(searchResults[iType].values()) 1314 if iTypeValuesCount > 0: 1315 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1316 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1317 1318 for instrument in searchResults[iType].values(): 1319 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1320 instrument["type"], 1321 instrument["ticker"], 1322 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1323 instrument["figi"], 1324 )) 1325 1326 if iTypeValuesCount <= 5: 1327 infoShort.extend(info[-iTypeValuesCount:]) 1328 1329 else: 1330 infoShort.extend(info[-5:]) 1331 infoShort.append(skippedLine) 1332 1333 infoText = "".join(info) 1334 infoTextShort = "".join(infoShort) 1335 1336 if show: 1337 uLogger.info(infoTextShort) 1338 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1339 1340 if self.searchResultsFile: 1341 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1342 fH.write(infoText) 1343 1344 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1345 1346 return searchResults 1347 1348 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1349 """ 1350 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1351 1352 :param instruments: list of strings with tickers or FIGIs. 1353 :return: list with unique instrument FIGIs only. 1354 """ 1355 requestedInstruments = [] 1356 for iName in instruments: 1357 if iName not in self.aliases.keys(): 1358 if iName not in requestedInstruments: 1359 requestedInstruments.append(iName) 1360 1361 else: 1362 if iName not in requestedInstruments: 1363 if self.aliases[iName] not in requestedInstruments: 1364 requestedInstruments.append(self.aliases[iName]) 1365 1366 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1367 1368 onlyUniqueFIGIs = [] 1369 for iName in requestedInstruments: 1370 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1371 continue 1372 1373 self.ticker = iName 1374 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1375 1376 if not iData: 1377 self.ticker = "" 1378 self.figi = iName 1379 1380 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1381 1382 if not iData: 1383 self.figi = "" 1384 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1385 1386 if iData and iData["figi"] not in onlyUniqueFIGIs: 1387 onlyUniqueFIGIs.append(iData["figi"]) 1388 1389 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1390 1391 return onlyUniqueFIGIs 1392 1393 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1394 """ 1395 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1396 See limits: https://tinkoff.github.io/investAPI/limits/ 1397 If `pricesFile` string is not empty then also save information to this file. 1398 1399 :param instruments: list of strings with tickers or FIGIs. 1400 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1401 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1402 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1403 """ 1404 if instruments is None or not instruments: 1405 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1406 raise Exception("Ticker or FIGI required") 1407 1408 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1409 1410 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1411 1412 iList = [] # trying to get info and current prices about all unique instruments: 1413 for self.figi in onlyUniqueFIGIs: 1414 iData = self.SearchByFIGI(requestPrice=True) 1415 iList.append(iData) 1416 1417 self.ShowListOfPrices(iList, show) 1418 1419 return iList 1420 1421 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1422 """ 1423 Show table contains current prices of given instruments. 1424 1425 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1427 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1428 :return: multilines text in Markdown format as a table contains current prices. 1429 """ 1430 infoText = "" 1431 1432 if show or self.pricesFile: 1433 info = [ 1434 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1435 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1436 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1437 ] 1438 1439 for item in iList: 1440 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1441 item["ticker"], 1442 item["figi"], 1443 item["type"], 1444 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1445 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1446 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1447 "{} / {}".format( 1448 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1449 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1450 ), 1451 "{} / {}".format( 1452 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1453 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1454 ), 1455 item["currency"], 1456 )) 1457 1458 infoText = "".join(info) 1459 1460 if show: 1461 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1462 1463 if self.pricesFile: 1464 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1465 fH.write(infoText) 1466 1467 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1468 1469 return infoText 1470 1471 def RequestTradingStatus(self) -> dict: 1472 """ 1473 Requesting trading status for the instrument defined by `figi` variable. 1474 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1475 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1476 1477 :return: dictionary with trading status attributes. Response example: 1478 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1479 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1480 """ 1481 if self.figi is None or not self.figi: 1482 uLogger.error("Variable `figi` must be defined for using this method!") 1483 raise Exception("FIGI required") 1484 1485 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1486 1487 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1488 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1489 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1490 1491 uLogger.debug("Records about current trading status successfully received") 1492 1493 return tradingStatus 1494 1495 def RequestPortfolio(self) -> dict: 1496 """ 1497 Requesting actual user's portfolio for current `accountId`. 1498 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1499 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1500 1501 :return: dictionary with user's portfolio. 1502 """ 1503 if self.accountId is None or not self.accountId: 1504 uLogger.error("Variable `accountId` must be defined for using this method!") 1505 raise Exception("Account ID required") 1506 1507 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1508 1509 self.body = str({"accountId": self.accountId}) 1510 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1511 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1512 1513 uLogger.debug("Records about user's portfolio successfully received") 1514 1515 return rawPortfolio 1516 1517 def RequestPositions(self) -> dict: 1518 """ 1519 Requesting open positions by currencies and instruments for current `accountId`. 1520 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1521 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1522 1523 :return: dictionary with open positions by instruments. 1524 """ 1525 if self.accountId is None or not self.accountId: 1526 uLogger.error("Variable `accountId` must be defined for using this method!") 1527 raise Exception("Account ID required") 1528 1529 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1530 1531 self.body = str({"accountId": self.accountId}) 1532 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1533 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1534 1535 uLogger.debug("Records about current open positions successfully received") 1536 1537 return rawPositions 1538 1539 def RequestPendingOrders(self) -> list: 1540 """ 1541 Requesting current actual pending orders for current `accountId`. 1542 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1543 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1544 1545 :return: list of dictionaries with pending orders. 1546 """ 1547 if self.accountId is None or not self.accountId: 1548 uLogger.error("Variable `accountId` must be defined for using this method!") 1549 raise Exception("Account ID required") 1550 1551 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1552 1553 self.body = str({"accountId": self.accountId}) 1554 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1555 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1556 1557 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1558 1559 return rawOrders 1560 1561 def RequestStopOrders(self) -> list: 1562 """ 1563 Requesting current actual stop orders for current `accountId`. 1564 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1565 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1566 1567 :return: list of dictionaries with stop orders. 1568 """ 1569 if self.accountId is None or not self.accountId: 1570 uLogger.error("Variable `accountId` must be defined for using this method!") 1571 raise Exception("Account ID required") 1572 1573 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1574 1575 self.body = str({"accountId": self.accountId}) 1576 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1577 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1578 1579 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1580 1581 return rawStopOrders 1582 1583 def Overview(self, show: bool = False, details: str = "full") -> dict: 1584 """ 1585 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1586 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1587 are defined then also save information to file. 1588 1589 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1590 many requests about the state of the portfolio, and then, based on the received data, a large number 1591 of calculation and statistics are collected. 1592 1593 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1594 :param details: how detailed should the information be? You should specify one of strings: 1595 `full` - shows full available information about portfolio status (by default), 1596 `positions` - shows only open positions, 1597 `digest` - show a short digest of the portfolio status, 1598 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1599 `orders` - shows only sections of open limits and stop orders. 1600 :return: dictionary with client's raw portfolio and some statistics. 1601 """ 1602 if self.accountId is None or not self.accountId: 1603 uLogger.error("Variable `accountId` must be defined for using this method!") 1604 raise Exception("Account ID required") 1605 1606 view = { 1607 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1608 "headers": {}, # list of dictionaries, response headers without "positions" section 1609 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1610 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1611 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1612 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1613 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1614 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1615 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1616 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1617 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1618 }, 1619 "stat": { # --- some statistics calculated using "raw" sections: 1620 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1621 "availableRUB": 0., # available rubles (without other currencies) 1622 "blockedRUB": 0., # blocked sum in Russian Rouble 1623 "totalChangesRUB": 0., # changes for all open trades in RUB 1624 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1625 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1626 "sharesCostRUB": 0., # costs of all shares in RUB 1627 "bondsCostRUB": 0., # costs of all bonds in RUB 1628 "etfsCostRUB": 0., # costs of all etfs in RUB 1629 "futuresCostRUB": 0., # costs of all futures in RUB 1630 "Currencies": [], # list of dictionaries of all currencies statistics 1631 "Shares": [], # list of dictionaries of all shares statistics 1632 "Bonds": [], # list of dictionaries of all bonds statistics 1633 "Etfs": [], # list of dictionaries of all etfs statistics 1634 "Futures": [], # list of dictionaries of all futures statistics 1635 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1636 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1637 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1638 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1639 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1640 }, 1641 "analytics": { # --- some analytics of portfolio: 1642 "distrByAssets": {}, # portfolio distribution by assets 1643 "distrByCompanies": {}, # portfolio distribution by companies 1644 "distrBySectors": {}, # portfolio distribution by sectors 1645 "distrByCurrencies": {}, # portfolio distribution by currencies 1646 "distrByCountries": {}, # portfolio distribution by countries 1647 } 1648 } 1649 1650 details = details.lower() 1651 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1652 if details not in availableDetails: 1653 details = "full" 1654 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1655 1656 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1657 1658 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1659 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1660 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1661 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1662 1663 # save response headers without "positions" section: 1664 for key in portfolioResponse.keys(): 1665 if key != "positions": 1666 view["raw"]["headers"][key] = portfolioResponse[key] 1667 1668 else: 1669 continue 1670 1671 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1672 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1673 for item in portfolioResponse["positions"]: 1674 if item["instrumentType"] == "currency": 1675 self.figi = item["figi"] 1676 curr = self.SearchByFIGI(requestPrice=False) 1677 1678 # current price of currency in RUB: 1679 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1680 "name": curr["name"], 1681 "currentPrice": NanoToFloat( 1682 item["currentPrice"]["units"], 1683 item["currentPrice"]["nano"] 1684 ), 1685 } 1686 1687 view["raw"]["Currencies"].append(item) 1688 1689 elif item["instrumentType"] == "share": 1690 view["raw"]["Shares"].append(item) 1691 1692 elif item["instrumentType"] == "bond": 1693 view["raw"]["Bonds"].append(item) 1694 1695 elif item["instrumentType"] == "etf": 1696 view["raw"]["Etfs"].append(item) 1697 1698 elif item["instrumentType"] == "futures": 1699 view["raw"]["Futures"].append(item) 1700 1701 else: 1702 continue 1703 1704 # how many volume of currencies (by ISO currency name) are blocked: 1705 for item in view["raw"]["positions"]["blocked"]: 1706 blocked = NanoToFloat(item["units"], item["nano"]) 1707 if blocked > 0: 1708 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1709 1710 # how many volume of instruments (by FIGI) are blocked: 1711 for item in view["raw"]["positions"]["securities"]: 1712 blocked = int(item["blocked"]) 1713 if blocked > 0: 1714 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1715 1716 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1717 1718 if "rub" in allBlocked.keys(): 1719 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1720 1721 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1722 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1723 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1724 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1725 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1726 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1727 view["stat"]["portfolioCostRUB"] = sum([ 1728 view["stat"]["allCurrenciesCostRUB"], 1729 view["stat"]["sharesCostRUB"], 1730 view["stat"]["bondsCostRUB"], 1731 view["stat"]["etfsCostRUB"], 1732 view["stat"]["futuresCostRUB"], 1733 ]) 1734 1735 # --- calculating some portfolio statistics: 1736 byComp = {} # distribution by companies 1737 bySect = {} # distribution by sectors 1738 byCurr = {} # distribution by currencies (include RUB) 1739 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1740 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1741 1742 for item in portfolioResponse["positions"]: 1743 self.figi = item["figi"] 1744 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1745 1746 if instrument: 1747 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1748 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1749 1750 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1751 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1752 1753 else: 1754 blocked = 0 1755 1756 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1757 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1758 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1759 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1760 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1761 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1762 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1763 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1764 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1765 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1766 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1767 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1768 1769 statData = { 1770 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1771 "ticker": instrument["ticker"], # ticker by FIGI 1772 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1773 "volume": volume, # available volume of instrument 1774 "lots": lots, # volume in lots of instrument 1775 "direction": direction, # direction of an instrument's position: short or long 1776 "blocked": blocked, # blocked volume of currency or instrument 1777 "currentPrice": curPrice, # current instrument's price in basic asset 1778 "average": average, # current average position price 1779 "cost": cost, # current cost of all volume of instrument in basic asset 1780 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1781 "costRUB": costRUB, # cost of instrument in ruble 1782 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1783 "profit": profit, # expected profit at current moment 1784 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1785 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1786 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1787 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1788 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1789 "step": instrument["step"], # minimum price increment 1790 } 1791 1792 # adding distribution by unique countries: 1793 if statData["country"] not in byCountry.keys(): 1794 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1795 1796 else: 1797 byCountry[statData["country"]]["cost"] += costRUB 1798 byCountry[statData["country"]]["percent"] += percentCostRUB 1799 1800 if item["instrumentType"] != "currency": 1801 # adding distribution by unique companies: 1802 if statData["name"]: 1803 if statData["name"] not in byComp.keys(): 1804 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1805 1806 else: 1807 byComp[statData["name"]]["cost"] += costRUB 1808 byComp[statData["name"]]["percent"] += percentCostRUB 1809 1810 # adding distribution by unique sectors: 1811 if statData["sector"] not in bySect.keys(): 1812 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1813 1814 else: 1815 bySect[statData["sector"]]["cost"] += costRUB 1816 bySect[statData["sector"]]["percent"] += percentCostRUB 1817 1818 # adding distribution by unique currencies: 1819 if currency not in byCurr.keys(): 1820 byCurr[currency] = { 1821 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1822 "cost": costRUB, 1823 "percent": percentCostRUB 1824 } 1825 1826 else: 1827 byCurr[currency]["cost"] += costRUB 1828 byCurr[currency]["percent"] += percentCostRUB 1829 1830 # saving statistics for every instrument: 1831 if item["instrumentType"] == "currency": 1832 view["stat"]["Currencies"].append(statData) 1833 1834 # update dict with free funds for trading (total - blocked) by currencies 1835 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1836 view["stat"]["funds"][currency] = { 1837 "total": volume, 1838 "totalCostRUB": costRUB, # total volume cost in rubles 1839 "free": volume - blocked, 1840 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1841 } 1842 1843 elif item["instrumentType"] == "share": 1844 view["stat"]["Shares"].append(statData) 1845 1846 elif item["instrumentType"] == "bond": 1847 view["stat"]["Bonds"].append(statData) 1848 1849 elif item["instrumentType"] == "etf": 1850 view["stat"]["Etfs"].append(statData) 1851 1852 elif item["instrumentType"] == "Futures": 1853 view["stat"]["Futures"].append(statData) 1854 1855 else: 1856 continue 1857 1858 # total changes in Russian Ruble: 1859 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1860 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1861 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1862 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1863 view["stat"]["funds"]["rub"] = { 1864 "total": view["stat"]["availableRUB"], 1865 "totalCostRUB": view["stat"]["availableRUB"], 1866 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1867 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1868 } 1869 1870 # --- pending orders sector data: 1871 uniquePendingOrders = [] 1872 uniquePendingOrdersFIGIs = [] 1873 for item in view["raw"]["orders"]: 1874 if item["figi"] not in uniquePendingOrdersFIGIs: 1875 uniquePendingOrdersFIGIs.append(item["figi"]) 1876 uniquePendingOrders.append(item) 1877 1878 for item in uniquePendingOrders: 1879 self.figi = item["figi"] 1880 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1881 1882 if instrument: 1883 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1884 orderType = TKS_ORDER_TYPES[item["orderType"]] 1885 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1886 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1887 1888 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1889 if item["direction"] == "ORDER_DIRECTION_BUY": 1890 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1891 1892 else: 1893 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1894 1895 # requested price for order execution: 1896 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1897 1898 # necessary changes in percent to reach target from current price: 1899 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1900 1901 view["stat"]["orders"].append({ 1902 "orderID": item["orderId"], # orderId number parameter of current order 1903 "figi": item["figi"], # FIGI identification 1904 "ticker": instrument["ticker"], # ticker name by FIGI 1905 "lotsRequested": item["lotsRequested"], # requested lots value 1906 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1907 "currentPrice": lastPrice, # current instrument's price for defined action 1908 "targetPrice": target, # requested price for order execution in base currency 1909 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1910 "percentChanges": changes, # changes in percent to target from current price 1911 "currency": item["currency"], # instrument's currency name 1912 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1913 "type": orderType, # type of order from TKS_ORDER_TYPES 1914 "status": orderState, # order status from TKS_ORDER_STATES 1915 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1916 }) 1917 1918 # --- stop orders sector data: 1919 uniqueStopOrders = [] 1920 uniqueStopOrdersFIGIs = [] 1921 for item in view["raw"]["stopOrders"]: 1922 if item["figi"] not in uniqueStopOrdersFIGIs: 1923 uniqueStopOrdersFIGIs.append(item["figi"]) 1924 uniqueStopOrders.append(item) 1925 1926 for item in uniqueStopOrders: 1927 self.figi = item["figi"] 1928 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1929 1930 if instrument: 1931 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1932 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1933 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1934 1935 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1936 if "expirationTime" in item.keys(): 1937 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1938 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1939 1940 else: 1941 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1942 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1943 1944 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1945 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1946 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1947 1948 else: 1949 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1950 1951 # requested price when stop-order executed: 1952 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1953 1954 # price for limit-order, set up when stop-order executed: 1955 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1956 1957 # necessary changes in percent to reach target from current price: 1958 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1959 1960 view["stat"]["stopOrders"].append({ 1961 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1962 "figi": item["figi"], # FIGI identification 1963 "ticker": instrument["ticker"], # ticker name by FIGI 1964 "lotsRequested": item["lotsRequested"], # requested lots value 1965 "currentPrice": lastPrice, # current instrument's price for defined action 1966 "targetPrice": target, # requested price for stop-order execution in base currency 1967 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1968 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1969 "percentChanges": changes, # changes in percent to target from current price 1970 "currency": item["currency"], # instrument's currency name 1971 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1972 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1973 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1974 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1975 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1976 }) 1977 1978 # --- calculating data for analytics section: 1979 # portfolio distribution by assets: 1980 view["analytics"]["distrByAssets"] = { 1981 "Ruble": { 1982 "uniques": 1, 1983 "cost": view["stat"]["availableRUB"], 1984 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1985 }, 1986 "Currencies": { 1987 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1988 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1989 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1990 }, 1991 "Shares": { 1992 "uniques": len(view["stat"]["Shares"]), 1993 "cost": view["stat"]["sharesCostRUB"], 1994 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1995 }, 1996 "Bonds": { 1997 "uniques": len(view["stat"]["Bonds"]), 1998 "cost": view["stat"]["bondsCostRUB"], 1999 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2000 }, 2001 "Etfs": { 2002 "uniques": len(view["stat"]["Etfs"]), 2003 "cost": view["stat"]["etfsCostRUB"], 2004 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2005 }, 2006 "Futures": { 2007 "uniques": len(view["stat"]["Futures"]), 2008 "cost": view["stat"]["futuresCostRUB"], 2009 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2010 }, 2011 } 2012 2013 # portfolio distribution by companies: 2014 view["analytics"]["distrByCompanies"]["All money cash"] = { 2015 "ticker": "", 2016 "cost": view["stat"]["allCurrenciesCostRUB"], 2017 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 } 2019 view["analytics"]["distrByCompanies"].update(byComp) 2020 2021 # portfolio distribution by sectors: 2022 view["analytics"]["distrBySectors"]["All money cash"] = { 2023 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2024 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2025 } 2026 view["analytics"]["distrBySectors"].update(bySect) 2027 2028 # portfolio distribution by currencies: 2029 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2030 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2031 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2032 2033 view["analytics"]["distrByCurrencies"].update(byCurr) 2034 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2035 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2036 2037 # portfolio distribution by countries: 2038 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2039 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2040 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2041 2042 view["analytics"]["distrByCountries"].update(byCountry) 2043 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2044 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2045 2046 # --- Prepare text statistics overview in human-readable: 2047 if show: 2048 # Whatever the value `details`, header not changes: 2049 info = [ 2050 "# Client's portfolio\n\n", 2051 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2052 "* **Account ID:** [{}]\n".format(self.accountId), 2053 ] 2054 2055 if details in ["full", "positions", "digest"]: 2056 info.extend([ 2057 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2058 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2059 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2060 view["stat"]["totalChangesRUB"], 2061 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2062 view["stat"]["totalChangesPercentRUB"], 2063 ), 2064 ]) 2065 2066 if details in ["full", "positions"]: 2067 info.extend([ 2068 "## Open positions\n\n", 2069 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2070 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2071 "| Ruble | {:>31} | | | | | |\n".format( 2072 "{:.2f} ({:.2f}) rub".format( 2073 view["stat"]["availableRUB"], 2074 view["stat"]["blockedRUB"], 2075 ) 2076 ) 2077 ]) 2078 2079 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2080 return [ 2081 "| | | | | | | |\n", 2082 "| {:<27} | | | | | {:>19} | |\n".format( 2083 noTradeStr if noTradeStr else typeStr, 2084 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2085 ), 2086 ] 2087 2088 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2089 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2090 "{} [{}]".format(data["ticker"], data["figi"]), 2091 "{:.2f} ({:.2f}) {}".format( 2092 data["volume"], 2093 data["blocked"], 2094 data["currency"], 2095 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2096 data["volume"], 2097 data["blocked"], 2098 ), 2099 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2100 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2101 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2102 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2103 "{}{:.2f} {} ({}{:.2f}%)".format( 2104 "+" if data["profit"] > 0 else "", 2105 data["profit"], data["baseCurrencyName"], 2106 "+" if data["percentProfit"] > 0 else "", 2107 data["percentProfit"], 2108 ), 2109 ) 2110 2111 # --- Show currencies section: 2112 if view["stat"]["Currencies"]: 2113 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2114 for item in view["stat"]["Currencies"]: 2115 info.append(_InfoStr(item, showCurrencyName=True)) 2116 2117 else: 2118 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2119 2120 # --- Show shares section: 2121 if view["stat"]["Shares"]: 2122 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2123 2124 for item in view["stat"]["Shares"]: 2125 info.append(_InfoStr(item)) 2126 2127 else: 2128 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2129 2130 # --- Show bonds section: 2131 if view["stat"]["Bonds"]: 2132 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2133 2134 for item in view["stat"]["Bonds"]: 2135 info.append(_InfoStr(item)) 2136 2137 else: 2138 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2139 2140 # --- Show etfs section: 2141 if view["stat"]["Etfs"]: 2142 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2143 2144 for item in view["stat"]["Etfs"]: 2145 info.append(_InfoStr(item)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2149 2150 # --- Show futures section: 2151 if view["stat"]["Futures"]: 2152 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2153 2154 for item in view["stat"]["Futures"]: 2155 info.append(_InfoStr(item)) 2156 2157 else: 2158 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2159 2160 if details in ["full", "orders"]: 2161 # --- Show pending orders section: 2162 if view["stat"]["orders"]: 2163 info.extend([ 2164 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2165 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2166 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2167 ]) 2168 2169 for item in view["stat"]["orders"]: 2170 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2171 "{} [{}]".format(item["ticker"], item["figi"]), 2172 item["orderID"], 2173 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2174 "{} {} ({}{:.2f}%)".format( 2175 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2176 item["baseCurrencyName"], 2177 "+" if item["percentChanges"] > 0 else "", 2178 float(item["percentChanges"]), 2179 ), 2180 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2181 item["action"], 2182 item["type"], 2183 item["date"], 2184 )) 2185 2186 else: 2187 info.append("\n## Total pending limit-orders: 0\n") 2188 2189 # --- Show stop orders section: 2190 if view["stat"]["stopOrders"]: 2191 info.extend([ 2192 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2193 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2194 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2195 ]) 2196 2197 for item in view["stat"]["stopOrders"]: 2198 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2199 "{} [{}]".format(item["ticker"], item["figi"]), 2200 item["orderID"], 2201 item["lotsRequested"], 2202 "{} {} ({}{:.2f}%)".format( 2203 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2204 item["baseCurrencyName"], 2205 "+" if item["percentChanges"] > 0 else "", 2206 float(item["percentChanges"]), 2207 ), 2208 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2209 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2210 item["action"], 2211 item["type"], 2212 item["expType"], 2213 item["createDate"], 2214 item["expDate"], 2215 )) 2216 2217 else: 2218 info.append("\n## Total stop-orders: 0\n") 2219 2220 if details in ["full", "analytics"]: 2221 # -- Show analytics section: 2222 if view["stat"]["portfolioCostRUB"] > 0: 2223 info.extend([ 2224 "\n# Analytics\n" 2225 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2226 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2227 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2228 view["stat"]["totalChangesRUB"], 2229 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2230 view["stat"]["totalChangesPercentRUB"], 2231 ), 2232 "\n## Portfolio distribution by assets\n" 2233 "\n| Type | Uniques | Percent | Current cost |\n", 2234 "|------------|---------|---------|--------------------|\n", 2235 ]) 2236 2237 for key in view["analytics"]["distrByAssets"].keys(): 2238 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2239 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2240 key, 2241 view["analytics"]["distrByAssets"][key]["uniques"], 2242 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2243 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2244 )) 2245 2246 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2247 info.extend([ 2248 "\n## Portfolio distribution by companies\n" 2249 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2250 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2251 ]) 2252 2253 for company in view["analytics"]["distrByCompanies"].keys(): 2254 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2255 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2256 info.append("| {} | {:<7} | {:<18} |\n".format( 2257 "{}{}{}".format( 2258 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2259 company, 2260 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2261 ), 2262 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2263 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2264 )) 2265 2266 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2267 info.extend([ 2268 "\n## Portfolio distribution by sectors\n" 2269 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2270 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2271 ]) 2272 2273 for sector in view["analytics"]["distrBySectors"].keys(): 2274 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2275 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2276 sector, 2277 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2278 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2279 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2280 )) 2281 2282 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2283 info.extend([ 2284 "\n## Portfolio distribution by currencies\n" 2285 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2286 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2287 ]) 2288 2289 for curr in view["analytics"]["distrByCurrencies"].keys(): 2290 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2291 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2292 info.append("| {} | {:<7} | {:<18} |\n".format( 2293 "[{}] {}{}".format( 2294 curr, 2295 view["analytics"]["distrByCurrencies"][curr]["name"], 2296 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2297 ), 2298 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2299 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2300 )) 2301 2302 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2303 info.extend([ 2304 "\n## Portfolio distribution by countries\n" 2305 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2306 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2307 ]) 2308 2309 for country in view["analytics"]["distrByCountries"].keys(): 2310 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2311 nameLen = len(country) 2312 info.append("| {} | {:<7} | {:<18} |\n".format( 2313 "{}{}".format( 2314 country, 2315 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2316 ), 2317 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2318 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2319 )) 2320 2321 infoText = "".join(info) 2322 2323 uLogger.info(infoText) 2324 2325 if details == "full" and self.overviewFile: 2326 filename = self.overviewFile 2327 2328 elif details == "digest" and self.overviewDigestFile: 2329 filename = self.overviewDigestFile 2330 2331 elif details == "positions" and self.overviewPositionsFile: 2332 filename = self.overviewPositionsFile 2333 2334 elif details == "orders" and self.overviewOrdersFile: 2335 filename = self.overviewOrdersFile 2336 2337 elif details == "analytics" and self.overviewAnalyticsFile: 2338 filename = self.overviewAnalyticsFile 2339 2340 else: 2341 filename = "" 2342 2343 if filename: 2344 with open(filename, "w", encoding="UTF-8") as fH: 2345 fH.write(infoText) 2346 2347 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2348 2349 return view 2350 2351 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2352 """ 2353 Returns history operations between two given dates for current `accountId`. 2354 If `reportFile` string is not empty then also save human-readable report. 2355 Shows some statistical data of closed positions. 2356 2357 :param start: see docstring in `GetDatesAsString()` method 2358 :param end: see docstring in `GetDatesAsString()` method 2359 :param show: if `True` then also prints all records to the console. 2360 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2361 :return: original list of dictionaries with history of deals records from API ("operations" key): 2362 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2363 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2364 """ 2365 if self.accountId is None or not self.accountId: 2366 uLogger.error("Variable `accountId` must be defined for using this method!") 2367 raise Exception("Account ID required") 2368 2369 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2370 2371 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2372 2373 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2374 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2375 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2376 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2377 customStat = {} # custom statistics in additional to responseJSON 2378 2379 # --- output report in human-readable format: 2380 if show or self.reportFile: 2381 splitLine1 = "| | | | | |\n" # Summary section 2382 splitLine2 = "| | | | | | | | |\n" # Operations section 2383 nextDay = "" 2384 2385 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2386 2387 if len(ops) > 0: 2388 customStat = { 2389 "opsCount": 0, # total operations count 2390 "buyCount": 0, # buy operations 2391 "sellCount": 0, # sell operations 2392 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2393 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2394 "payIn": {"rub": 0.}, # Deposit brokerage account 2395 "payOut": {"rub": 0.}, # Withdrawals 2396 "divs": {"rub": 0.}, # Dividends income 2397 "coupons": {"rub": 0.}, # Coupon's income 2398 "brokerCom": {"rub": 0.}, # Service commissions 2399 "serviceCom": {"rub": 0.}, # Service commissions 2400 "marginCom": {"rub": 0.}, # Margin commissions 2401 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2402 } 2403 2404 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2405 for item in ops: 2406 if item["state"] == "OPERATION_STATE_EXECUTED": 2407 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2408 2409 # count buy operations: 2410 if "_BUY" in item["operationType"]: 2411 customStat["buyCount"] += 1 2412 2413 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2414 customStat["buyTotal"][item["payment"]["currency"]] += payment 2415 2416 else: 2417 customStat["buyTotal"][item["payment"]["currency"]] = payment 2418 2419 # count sell operations: 2420 elif "_SELL" in item["operationType"]: 2421 customStat["sellCount"] += 1 2422 2423 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2424 customStat["sellTotal"][item["payment"]["currency"]] += payment 2425 2426 else: 2427 customStat["sellTotal"][item["payment"]["currency"]] = payment 2428 2429 # count incoming operations: 2430 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2431 if item["payment"]["currency"] in customStat["payIn"].keys(): 2432 customStat["payIn"][item["payment"]["currency"]] += payment 2433 2434 else: 2435 customStat["payIn"][item["payment"]["currency"]] = payment 2436 2437 # count withdrawals operations: 2438 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2439 if item["payment"]["currency"] in customStat["payOut"].keys(): 2440 customStat["payOut"][item["payment"]["currency"]] += payment 2441 2442 else: 2443 customStat["payOut"][item["payment"]["currency"]] = payment 2444 2445 # count dividends income: 2446 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2447 if item["payment"]["currency"] in customStat["divs"].keys(): 2448 customStat["divs"][item["payment"]["currency"]] += payment 2449 2450 else: 2451 customStat["divs"][item["payment"]["currency"]] = payment 2452 2453 # count coupon's income: 2454 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2455 if item["payment"]["currency"] in customStat["coupons"].keys(): 2456 customStat["coupons"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["coupons"][item["payment"]["currency"]] = payment 2460 2461 # count broker commissions: 2462 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2463 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2464 customStat["brokerCom"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["brokerCom"][item["payment"]["currency"]] = payment 2468 2469 # count service commissions: 2470 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2471 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2472 customStat["serviceCom"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["serviceCom"][item["payment"]["currency"]] = payment 2476 2477 # count margin commissions: 2478 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2479 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2480 customStat["marginCom"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["marginCom"][item["payment"]["currency"]] = payment 2484 2485 # count withholding taxes: 2486 elif "_TAX" in item["operationType"]: 2487 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2488 customStat["allTaxes"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["allTaxes"][item["payment"]["currency"]] = payment 2492 2493 else: 2494 continue 2495 2496 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2497 2498 # --- view "Actions" lines: 2499 info.extend([ 2500 "| 1 | 2 | 3 | 4 | 5 |\n", 2501 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2502 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2503 "| | Buy: {:<22} | {:<28} | | |\n".format( 2504 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2505 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2506 ), 2507 "| | Sell: {:<21} | {:<28} | | |\n".format( 2508 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2509 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2510 ), 2511 ]) 2512 2513 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2514 for key in opsKeys: 2515 if key == "rub": 2516 continue 2517 2518 info.extend([ 2519 "| | | {:<28} | | |\n".format( 2520 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2521 ), 2522 "| | | {:<28} | | |\n".format( 2523 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2524 ), 2525 ]) 2526 2527 info.append(splitLine1) 2528 2529 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2530 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2531 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2532 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2533 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2534 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2535 ) 2536 2537 # --- view "Payments" lines: 2538 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2539 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2540 2541 for key in paymentsKeys: 2542 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2543 2544 info.append(splitLine1) 2545 2546 # --- view "Commissions and taxes" lines: 2547 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2548 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2549 2550 for key in comKeys: 2551 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2552 2553 info.append(splitLine1) 2554 2555 info.extend([ 2556 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2557 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2558 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2559 ]) 2560 2561 else: 2562 info.append("Broker returned no operations during this period\n") 2563 2564 # --- view "Operations" section: 2565 for item in ops: 2566 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2567 continue 2568 2569 else: 2570 self.figi = item["figi"] if item["figi"] else "" 2571 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2572 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2573 2574 # group of deals during one day: 2575 if nextDay and item["date"].split("T")[0] != nextDay: 2576 info.append(splitLine2) 2577 nextDay = "" 2578 2579 else: 2580 nextDay = item["date"].split("T")[0] # saving current day for splitting 2581 2582 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2583 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2584 self.figi if self.figi else "—", 2585 instrument["ticker"] if instrument else "—", 2586 instrument["type"] if instrument else "—", 2587 item["quantity"] if int(item["quantity"]) > 0 else "—", 2588 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2589 TKS_OPERATION_STATES[item["state"]], 2590 TKS_OPERATION_TYPES[item["operationType"]], 2591 )) 2592 2593 infoText = "".join(info) 2594 2595 if show: 2596 uLogger.info(infoText) 2597 2598 if self.reportFile: 2599 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2600 fH.write(infoText) 2601 2602 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2603 2604 return ops, customStat 2605 2606 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2607 """ 2608 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2609 2610 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2611 Warning! Broker server used ISO UTC time by default. 2612 2613 If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe. 2614 Also, `historyFile` used to update history with `onlyMissing` parameter. 2615 2616 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2617 2618 :param start: see docstring in `GetDatesAsString()` method. 2619 :param end: see docstring in `GetDatesAsString()` method. 2620 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2621 `"hour"`, `"day"`. Default: `"hour"`. 2622 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2623 False by default. Warning! History appends only from last candle to current time 2624 with always update last candle! 2625 :param csvSep: separator if csv-file is used, `,` by default. 2626 :param show: if `True` then also prints pandas dataframe to the console. 2627 :return: pandas dataframe with prices history. Headers of columns are defined by default: 2628 `["date", "time", "open", "high", "low", "close", "volume"]`. 2629 """ 2630 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2631 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2632 history = None # empty pandas object for history 2633 2634 if interval not in TKS_CANDLE_INTERVALS.keys(): 2635 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2636 raise Exception("Incorrect value") 2637 2638 if not (self.ticker or self.figi): 2639 uLogger.error("Ticker or FIGI must be defined!") 2640 raise Exception("Ticker or FIGI required") 2641 2642 if self.ticker and not self.figi: 2643 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2644 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2645 2646 if self.figi and not self.ticker: 2647 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2648 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2649 2650 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2651 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2652 if interval.lower() != "day": 2653 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2654 2655 delta = dtEnd - dtStart # current UTC time minus last time in file 2656 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2657 2658 # calculate history length in candles: 2659 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2660 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2661 length += 1 # to avoid fraction time 2662 2663 # calculate data blocks count: 2664 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2665 2666 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2667 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2668 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2669 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2670 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2671 2672 tempOld = None # pandas object for old history, if --only-missing key present 2673 lastTime = None # datetime object of last old candle in file 2674 2675 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2676 uLogger.debug("--only-missing key present, add only last missing candles...") 2677 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2678 2679 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2680 2681 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2682 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2683 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2684 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2685 2686 # get last datetime object from last string in file or minus 1 delta if file is empty: 2687 if len(tempOld) > 0: 2688 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2689 2690 else: 2691 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2692 2693 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2694 2695 responseJSONs = [] # raw history blocks of data 2696 2697 blockEnd = dtEnd 2698 for item in range(blocks): 2699 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2700 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2701 2702 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2703 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2704 )) 2705 2706 if blockStart == blockEnd: 2707 uLogger.debug("Skipped this zero-length block...") 2708 2709 else: 2710 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2711 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2712 self.body = str({ 2713 "figi": self.figi, 2714 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2715 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2716 "interval": TKS_CANDLE_INTERVALS[interval][0] 2717 }) 2718 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2719 2720 if "code" in responseJSON.keys(): 2721 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2722 2723 else: 2724 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2725 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2726 2727 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2728 2729 blockEnd = blockStart 2730 2731 printCount = len(responseJSONs) # candles to show in console 2732 if responseJSONs: 2733 tempHistory = pd.DataFrame( 2734 data={ 2735 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2736 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2737 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2738 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2739 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2740 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2741 "volume": [int(item["volume"]) for item in responseJSONs], 2742 }, 2743 index=range(len(responseJSONs)), 2744 columns=["date", "time", "open", "high", "low", "close", "volume"], 2745 ) 2746 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2747 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2748 2749 # append only newest candles to old history if --only-missing key present: 2750 if onlyMissing and tempOld is not None and lastTime is not None: 2751 index = 0 # find start index in tempHistory data: 2752 2753 for i, item in tempHistory.iterrows(): 2754 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2755 2756 if curTime == lastTime: 2757 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2758 index = i 2759 printCount = index + 1 2760 break 2761 2762 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2763 2764 else: 2765 history = tempHistory # if no `--only-missing` key then load full data from server 2766 2767 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2768 2769 if history is not None and not history.empty: 2770 if show: 2771 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2772 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2773 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2774 )) 2775 2776 else: 2777 uLogger.warning("Received an empty candles history!") 2778 2779 if self.historyFile is not None: 2780 if history is not None and not history.empty: 2781 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2782 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2783 2784 else: 2785 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2786 2787 else: 2788 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.") 2789 2790 return history 2791 2792 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2793 """ 2794 Load candles history from csv-file and return pandas dataframe object. 2795 2796 See also: `History()` and `ShowHistoryChart()` methods. 2797 2798 :param filePath: path to csv-file to open. 2799 """ 2800 loadedHistory = None # init candles data object 2801 2802 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2803 2804 if os.path.exists(filePath): 2805 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as pandas dataframe 2806 2807 tfStr = self.priceModel.FormattedDelta( 2808 self.priceModel.timeframe, 2809 "{days} days {hours}h {minutes}m {seconds}s", 2810 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2811 self.priceModel.timeframe, 2812 "{hours}h {minutes}m {seconds}s", 2813 ) 2814 2815 if loadedHistory is not None and not loadedHistory.empty: 2816 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2817 len(loadedHistory), 2818 tfStr, 2819 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2820 ) 2821 2822 else: 2823 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2824 2825 else: 2826 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2827 2828 return loadedHistory 2829 2830 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2831 """ 2832 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2833 2834 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2835 Default: `index.html` (both for interact and non-interact candlesticks chart). 2836 2837 See also: `History()` and `LoadHistory()` methods. 2838 2839 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2840 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2841 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2842 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2843 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2844 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2845 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2846 """ 2847 if isinstance(candles, str): 2848 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2849 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2850 2851 elif isinstance(candles, pd.DataFrame): 2852 self.priceModel.prices = candles # set candles chain from variable 2853 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2854 2855 if "datetime" not in candles.columns: 2856 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2857 2858 else: 2859 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2860 raise Exception("Incorrect value") 2861 2862 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2863 2864 if interact: 2865 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2866 2867 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2868 2869 else: 2870 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2871 2872 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2873 2874 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2875 2876 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2877 """ 2878 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2879 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2880 2881 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2882 2883 :param operation: string "Buy" or "Sell". 2884 :param lots: volume, integer count of lots >= 1. 2885 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2886 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2887 :param expDate: string "Undefined" by default or local date in future, 2888 it is a string with format `%Y-%m-%d %H:%M:%S`. 2889 :return: JSON with response from broker server. 2890 """ 2891 if self.accountId is None or not self.accountId: 2892 uLogger.error("Variable `accountId` must be defined for using this method!") 2893 raise Exception("Account ID required") 2894 2895 if operation is None or not operation or operation not in ("Buy", "Sell"): 2896 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2897 raise Exception("Incorrect value") 2898 2899 if lots is None or lots < 1: 2900 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2901 lots = 1 2902 2903 if tp is None or tp < 0: 2904 tp = 0 2905 2906 if sl is None or sl < 0: 2907 sl = 0 2908 2909 if expDate is None or not expDate: 2910 expDate = "Undefined" 2911 2912 if not (self.ticker or self.figi): 2913 uLogger.error("Ticker or FIGI must be defined!") 2914 raise Exception("Ticker or FIGI required") 2915 2916 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2917 self.ticker = instrument["ticker"] 2918 self.figi = instrument["figi"] 2919 2920 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2921 2922 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2923 self.body = str({ 2924 "figi": self.figi, 2925 "quantity": str(lots), 2926 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2927 "accountId": str(self.accountId), 2928 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2929 }) 2930 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2931 2932 if "orderId" in response.keys(): 2933 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2934 operation, response["orderId"], 2935 self.ticker, self.figi, lots, 2936 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2937 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2938 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2939 )) 2940 2941 else: 2942 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2943 2944 if tp > 0: 2945 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2946 2947 if sl > 0: 2948 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2949 2950 return response 2951 2952 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2953 """ 2954 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2955 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2956 2957 See also: `Order()` and `Trade()` docstrings. 2958 2959 :param lots: volume, integer count of lots >= 1. 2960 :param tp: float > 0, take profit price of stop-order. 2961 :param sl: float > 0, stop loss price of stop-order. 2962 :param expDate: it's a local date in future. 2963 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2964 :return: JSON with response from broker server. 2965 """ 2966 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2967 2968 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2969 """ 2970 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2971 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2972 2973 See also: `Order()` and `Trade()` docstrings. 2974 2975 :param lots: volume, integer count of lots >= 1. 2976 :param tp: float > 0, take profit price of stop-order. 2977 :param sl: float > 0, stop loss price of stop-order. 2978 :param expDate: it's a local date in the future. 2979 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2980 :return: JSON with response from broker server. 2981 """ 2982 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 2983 2984 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2985 """ 2986 Close position of given instruments. 2987 2988 :param tickers: tickers list of instruments that must be closed. 2989 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2990 This avoids unnecessary downloading data from the server. 2991 """ 2992 if not tickers: 2993 uLogger.info("Tickers list is empty, nothing to close.") 2994 2995 else: 2996 if portfolio is None or not portfolio: 2997 portfolio = self.Overview(show=False) 2998 2999 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3000 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3001 3002 for ticker in tickers: 3003 if ticker not in allOpenedTickers: 3004 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3005 continue 3006 3007 # search open trade info about instrument by ticker: 3008 instrument = {} 3009 for iType in TKS_INSTRUMENTS: 3010 if instrument: 3011 break 3012 3013 for item in portfolio["stat"][iType]: 3014 if item["ticker"] == ticker: 3015 instrument = item 3016 break 3017 3018 if instrument: 3019 self.ticker = ticker 3020 self.figi = instrument["figi"] 3021 3022 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3023 self.ticker, 3024 self.figi, 3025 int(instrument["volume"]), 3026 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3027 )) 3028 3029 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3030 3031 if tradeLots > 0: 3032 if instrument["blocked"] > 0: 3033 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3034 instrument["blocked"], 3035 self.ticker, 3036 tradeLots, 3037 )) 3038 3039 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3040 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3041 3042 else: 3043 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3044 3045 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3046 """ 3047 Close all positions of given instruments with defined type. 3048 3049 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3050 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3051 This avoids unnecessary downloading data from the server. 3052 """ 3053 if iType not in TKS_INSTRUMENTS: 3054 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3055 3056 else: 3057 if portfolio is None or not portfolio: 3058 portfolio = self.Overview(show=False) 3059 3060 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3061 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3062 3063 if tickers and portfolio: 3064 self.CloseTrades(tickers, portfolio) 3065 3066 else: 3067 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3068 3069 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3070 """ 3071 Universal method to create market or limit orders with all available parameters for current `accountId`. 3072 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3073 3074 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3075 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3076 3077 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3078 then broker immediately open market order as you can do simple --buy or --sell operations! 3079 3080 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3081 When current price will go up or down to target price value then broker opens a limit order. 3082 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3083 3084 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3085 3086 :param operation: string "Buy" or "Sell". 3087 :param orderType: string "Limit" or "Stop". 3088 :param lots: volume, integer count of lots >= 1. 3089 :param targetPrice: target price > 0. This is open trade price for limit order. 3090 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3091 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3092 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3093 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3094 Stop loss order always executed by market price. 3095 :param expDate: string "Undefined" by default or local date in future. 3096 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3097 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3098 A limit order has no expiration date, it lasts until the end of the trading day. 3099 :return: JSON with response from broker server. 3100 """ 3101 if self.accountId is None or not self.accountId: 3102 uLogger.error("Variable `accountId` must be defined for using this method!") 3103 raise Exception("Account ID required") 3104 3105 if operation is None or not operation or operation not in ("Buy", "Sell"): 3106 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3107 raise Exception("Incorrect value") 3108 3109 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3110 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3111 raise Exception("Incorrect value") 3112 3113 if lots is None or lots < 1: 3114 uLogger.error("You must define trade volume > 0: integer count of lots!") 3115 raise Exception("Incorrect value") 3116 3117 if targetPrice is None or targetPrice <= 0: 3118 uLogger.error("Target price for limit-order must be greater than 0!") 3119 raise Exception("Incorrect value") 3120 3121 if limitPrice is None or limitPrice <= 0: 3122 limitPrice = targetPrice 3123 3124 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3125 stopType = "Limit" 3126 3127 if expDate is None or not expDate: 3128 expDate = "Undefined" 3129 3130 if not (self.ticker or self.figi): 3131 uLogger.error("Tocker or FIGI must be defined!") 3132 raise Exception("Ticker or FIGI required") 3133 3134 response = {} 3135 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3136 self.ticker = instrument["ticker"] 3137 self.figi = instrument["figi"] 3138 3139 if orderType == "Limit": 3140 uLogger.debug( 3141 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3142 self.ticker, self.figi, 3143 operation, lots, targetPrice, instrument["currency"], 3144 )) 3145 3146 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3147 self.body = str({ 3148 "figi": self.figi, 3149 "quantity": str(lots), 3150 "price": FloatToNano(targetPrice), 3151 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3152 "accountId": str(self.accountId), 3153 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3154 }) 3155 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3156 3157 if "orderId" in response.keys(): 3158 uLogger.info( 3159 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3160 response["orderId"], 3161 self.ticker, self.figi, 3162 operation, lots, targetPrice, instrument["currency"], 3163 )) 3164 3165 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3166 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3167 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3168 targetPrice, instrument["currency"], 3169 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3170 )) 3171 3172 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3173 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3174 targetPrice, instrument["currency"], 3175 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3176 )) 3177 3178 else: 3179 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3180 3181 if orderType == "Stop": 3182 uLogger.debug( 3183 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3184 self.ticker, self.figi, 3185 operation, lots, 3186 targetPrice, instrument["currency"], 3187 limitPrice, instrument["currency"], 3188 stopType, expDate, 3189 )) 3190 3191 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3192 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3193 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3194 3195 body = { 3196 "figi": self.figi, 3197 "quantity": str(lots), 3198 "price": FloatToNano(limitPrice), 3199 "stopPrice": FloatToNano(targetPrice), 3200 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3201 "accountId": str(self.accountId), 3202 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3203 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3204 } 3205 3206 if expDateUTC: 3207 body["expireDate"] = expDateUTC 3208 3209 self.body = str(body) 3210 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3211 3212 if "stopOrderId" in response.keys(): 3213 uLogger.info( 3214 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3215 response["stopOrderId"], 3216 self.ticker, self.figi, 3217 operation, lots, 3218 targetPrice, instrument["currency"], 3219 limitPrice, instrument["currency"], 3220 TKS_STOP_ORDER_TYPES[stopOrderType], 3221 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3222 )) 3223 3224 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3225 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3226 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3227 targetPrice, instrument["currency"], 3228 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3229 )) 3230 3231 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3232 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3233 targetPrice, instrument["currency"], 3234 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3235 )) 3236 3237 else: 3238 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3239 3240 return response 3241 3242 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3243 """ 3244 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3245 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3246 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3247 See also: `Order()` docstring. 3248 3249 :param lots: volume, integer count of lots >= 1. 3250 :param targetPrice: target price > 0. This is open trade price for limit order. 3251 :return: JSON with response from broker server. 3252 """ 3253 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3254 3255 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3256 """ 3257 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3258 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3259 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3260 target price value then broker opens a limit order. See also: `Order()` docstring. 3261 3262 :param lots: volume, integer count of lots >= 1. 3263 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3264 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3265 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3266 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3267 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3268 :param expDate: string "Undefined" by default or local date in future. 3269 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3270 This date is converting to UTC format for server. 3271 :return: JSON with response from broker server. 3272 """ 3273 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3274 3275 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3276 """ 3277 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3278 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3279 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3280 See also: `Order()` docstring. 3281 3282 :param lots: volume, integer count of lots >= 1. 3283 :param targetPrice: target price > 0. This is open trade price for limit order. 3284 :return: JSON with response from broker server. 3285 """ 3286 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3287 3288 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3289 """ 3290 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3291 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3292 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3293 target price value then broker opens a limit order. See also: `Order()` docstring. 3294 3295 :param lots: volume, integer count of lots >= 1. 3296 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3297 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3298 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3299 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3300 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3301 :param expDate: string "Undefined" by default or local date in future. 3302 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3303 This date is converting to UTC format for server. 3304 :return: JSON with response from broker server. 3305 """ 3306 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3307 3308 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3309 """ 3310 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3311 3312 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3313 :param allOrdersIDs: pre-received lists of all active pending orders. 3314 This avoids unnecessary downloading data from the server. 3315 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3316 """ 3317 if self.accountId is None or not self.accountId: 3318 uLogger.error("Variable `accountId` must be defined for using this method!") 3319 raise Exception("Account ID required") 3320 3321 if orderIDs: 3322 if allOrdersIDs is None or not allOrdersIDs: 3323 rawOrders = self.RequestPendingOrders() 3324 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3325 3326 if allStopOrdersIDs is None or not allStopOrdersIDs: 3327 rawStopOrders = self.RequestStopOrders() 3328 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3329 3330 for orderID in orderIDs: 3331 idInPendingOrders = orderID in allOrdersIDs 3332 idInStopOrders = orderID in allStopOrdersIDs 3333 3334 if not (idInPendingOrders or idInStopOrders): 3335 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3336 continue 3337 3338 else: 3339 if idInPendingOrders: 3340 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3341 3342 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3343 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3344 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3345 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3346 3347 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3348 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3349 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3350 3351 else: 3352 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3353 3354 elif idInStopOrders: 3355 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3356 3357 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3358 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3359 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3360 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3361 3362 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3363 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3364 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3365 3366 else: 3367 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3368 3369 else: 3370 continue 3371 3372 def CloseAllOrders(self) -> None: 3373 """ 3374 Gets a list of open pending and stop orders and cancel it all. 3375 """ 3376 rawOrders = self.RequestPendingOrders() 3377 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3378 lenOrders = len(allOrdersIDs) 3379 3380 rawStopOrders = self.RequestStopOrders() 3381 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3382 lenSOrders = len(allStopOrdersIDs) 3383 3384 if lenOrders > 0 or lenSOrders > 0: 3385 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3386 3387 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3388 3389 else: 3390 uLogger.info("Orders not found, nothing to cancel.") 3391 3392 def CloseAll(self, *args) -> None: 3393 """ 3394 Close all available (not blocked) opened trades and orders. 3395 3396 Also, you can select one or more keywords case-insensitive: 3397 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3398 3399 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3400 """ 3401 overview = self.Overview(show=False) # get all open trades info 3402 3403 if len(args) == 0: 3404 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3405 self.CloseAllOrders() # close all pending and stop orders 3406 3407 for iType in TKS_INSTRUMENTS: 3408 if iType != "Currencies": 3409 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3410 3411 else: 3412 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3413 lowerArgs = [x.lower() for x in args] 3414 3415 if "orders" in lowerArgs: 3416 self.CloseAllOrders() # close all pending and stop orders 3417 3418 for iType in TKS_INSTRUMENTS: 3419 if iType.lower() in lowerArgs and iType != "Currencies": 3420 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3421 3422 @staticmethod 3423 def ParseOrderParameters(operation, **inputParameters): 3424 """ 3425 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3426 3427 :param operation: string "Buy" or "Sell". 3428 :param inputParameters: this is dict of strings that looks like this 3429 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3430 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3431 "prices" key: one or more prices to open limit-orders 3432 Counts of values in lots and prices lists must be equals! 3433 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3434 """ 3435 # TODO: update order grid work with api v2 3436 pass 3437 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3438 # 3439 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3440 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3441 # raise Exception("Incorrect value") 3442 # 3443 # if "l" in inputParameters.keys(): 3444 # inputParameters["lots"] = inputParameters.pop("l") 3445 # 3446 # if "p" in inputParameters.keys(): 3447 # inputParameters["prices"] = inputParameters.pop("p") 3448 # 3449 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3450 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3451 # raise Exception("Incorrect value") 3452 # 3453 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3454 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3455 # 3456 # if len(lots) != len(prices): 3457 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3458 # raise Exception("Incorrect value") 3459 # 3460 # uLogger.debug("Extracted parameters for orders:") 3461 # uLogger.debug("lots = {}".format(lots)) 3462 # uLogger.debug("prices = {}".format(prices)) 3463 # 3464 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3465 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3466 # uLogger.debug("Order parameters: {}".format(result)) 3467 # 3468 # return result 3469 3470 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3471 """ 3472 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3473 3474 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3475 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3476 """ 3477 result = False 3478 msg = "Instrument not defined!" 3479 3480 if portfolio is None or not portfolio: 3481 portfolio = self.Overview(show=False) 3482 3483 if self.ticker: 3484 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3485 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3486 3487 for iType in TKS_INSTRUMENTS: 3488 for instrument in portfolio["stat"][iType]: 3489 if instrument["ticker"] == self.ticker: 3490 result = True 3491 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3492 break 3493 3494 elif self.figi: 3495 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3496 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3497 3498 for iType in TKS_INSTRUMENTS: 3499 for instrument in portfolio["stat"][iType]: 3500 if instrument["figi"] == self.figi: 3501 result = True 3502 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3503 break 3504 3505 else: 3506 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3507 3508 uLogger.debug(msg) 3509 3510 return result 3511 3512 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3513 """ 3514 Returns instrument is in the user's portfolio if it presents there. 3515 Instrument must be defined by `ticker` (highly priority) or `figi`. 3516 3517 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3518 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3519 """ 3520 result = None 3521 msg = "Instrument not defined!" 3522 3523 if portfolio is None or not portfolio: 3524 portfolio = self.Overview(show=False) 3525 3526 if self.ticker: 3527 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3528 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3529 3530 for iType in TKS_INSTRUMENTS: 3531 for instrument in portfolio["stat"][iType]: 3532 if instrument["ticker"] == self.ticker: 3533 result = instrument 3534 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3535 break 3536 3537 elif self.figi: 3538 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3539 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3540 3541 for iType in TKS_INSTRUMENTS: 3542 for instrument in portfolio["stat"][iType]: 3543 if instrument["figi"] == self.figi: 3544 result = instrument 3545 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3546 break 3547 3548 else: 3549 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3550 3551 uLogger.debug(msg) 3552 3553 return result 3554 3555 def RequestLimits(self) -> dict: 3556 """ 3557 Method for obtaining the available funds for withdrawal for current `accountId`. 3558 3559 See also: 3560 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3561 - `OverviewLimits()` method 3562 3563 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3564 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3565 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3566 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3567 """ 3568 if self.accountId is None or not self.accountId: 3569 uLogger.error("Variable `accountId` must be defined for using this method!") 3570 raise Exception("Account ID required") 3571 3572 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3573 3574 self.body = str({"accountId": self.accountId}) 3575 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3576 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3577 3578 uLogger.debug("Records about available funds for withdrawal successfully received") 3579 3580 return rawLimits 3581 3582 def OverviewLimits(self, show: bool = False) -> dict: 3583 """ 3584 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3585 3586 See also: `RequestLimits()`. 3587 3588 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3589 :return: dict with raw parsed data from server and some calculated statistics about it. 3590 """ 3591 if self.accountId is None or not self.accountId: 3592 uLogger.error("Variable `accountId` must be defined for using this method!") 3593 raise Exception("Account ID required") 3594 3595 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3596 3597 view = { 3598 "rawLimits": rawLimits, 3599 "limits": { # parsed data for every currency: 3600 "money": { # this is an array of portfolio currency positions 3601 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3602 }, 3603 "blocked": { # this is an array of blocked currency 3604 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3605 }, 3606 "blockedGuarantee": { # this is locked money under collateral for futures 3607 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3608 }, 3609 }, 3610 } 3611 3612 # --- Prepare text table with limits in human-readable format: 3613 if show: 3614 info = [ 3615 "# Withdrawal limits\n\n", 3616 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3617 "* **Account ID:** [{}]\n".format(self.accountId), 3618 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3619 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3620 ] 3621 3622 for curr in view["limits"]["money"].keys(): 3623 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3624 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3625 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3626 3627 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3628 "[{}]".format(curr), 3629 "{:.2f}".format(view["limits"]["money"][curr]), 3630 "{:.2f}".format(availableMoney), 3631 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3632 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3633 ) 3634 3635 if curr == "rub": 3636 info.insert(5, infoStr) # insert at first position in table and after headers 3637 3638 else: 3639 info.append(infoStr) 3640 3641 infoText = "".join(info) 3642 3643 uLogger.info(infoText) 3644 3645 if self.withdrawalLimitsFile: 3646 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3647 fH.write(infoText) 3648 3649 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3650 3651 return view 3652 3653 def RequestAccounts(self) -> dict: 3654 """ 3655 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3656 3657 See also: 3658 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3659 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3660 - `OverviewUserInfo()` method 3661 3662 :return: dict with raw data from server that contains accounts info. Example of dict: 3663 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3664 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3665 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3666 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3667 """ 3668 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3669 3670 self.body = str({}) 3671 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3672 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3673 3674 uLogger.debug("Records about available accounts successfully received") 3675 3676 return rawAccounts 3677 3678 def RequestUserInfo(self) -> dict: 3679 """ 3680 Method for requesting common user's information. 3681 3682 See also: 3683 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3684 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3685 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3686 - `OverviewUserInfo()` method 3687 3688 :return: dict with raw data from server that contains user's information. Example of dict: 3689 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3690 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3691 """ 3692 uLogger.debug("Requesting common user's information. Wait, please...") 3693 3694 self.body = str({}) 3695 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3696 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3697 3698 uLogger.debug("Records about current user successfully received") 3699 3700 return rawUserInfo 3701 3702 def RequestMarginStatus(self, accountId: str = None) -> dict: 3703 """ 3704 Method for requesting margin calculation for defined account ID. 3705 3706 See also: 3707 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3708 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3709 - `OverviewUserInfo()` method 3710 3711 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3712 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3713 Example of responses: 3714 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3715 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3716 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3717 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3718 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3719 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3720 """ 3721 if accountId is None or not accountId: 3722 if self.accountId is None or not self.accountId: 3723 uLogger.error("Variable `accountId` must be defined for using this method!") 3724 raise Exception("Account ID required") 3725 3726 else: 3727 accountId = self.accountId # use `self.accountId` (main ID) by default 3728 3729 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3730 3731 self.body = str({"accountId": accountId}) 3732 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3733 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3734 3735 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3736 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3737 rawMargin = {} 3738 3739 else: 3740 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3741 3742 return rawMargin 3743 3744 def RequestTariffLimits(self) -> dict: 3745 """ 3746 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3747 3748 See also: 3749 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3750 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3751 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3752 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3753 - `OverviewUserInfo()` method 3754 3755 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3756 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3757 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3758 """ 3759 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3760 3761 self.body = str({}) 3762 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3763 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3764 3765 uLogger.debug("Records with limits of current tariff successfully received") 3766 3767 return rawTariffLimits 3768 3769 def RequestBondCoupons(self, iJSON: dict) -> dict: 3770 """ 3771 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3772 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3773 All dates are in UTC timezone. 3774 3775 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3776 Documentation: 3777 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3778 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3779 3780 See also: `ExtendBondsData()`. 3781 3782 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3783 If raw iJSON is not data of bond then server returns an error [400] with message: 3784 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3785 :return: dictionary with bond payment calendar. Response example 3786 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3787 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3788 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3789 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3790 """ 3791 if iJSON["figi"] is None or not iJSON["figi"]: 3792 uLogger.error("FIGI must be defined for using this method!") 3793 raise Exception("FIGI required") 3794 3795 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3796 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3797 3798 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3799 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3800 self.figi, 3801 startDate, 3802 endDate, 3803 )) 3804 3805 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3806 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3807 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3808 3809 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3810 uLogger.warning("Instrument type is not bond!") 3811 3812 else: 3813 uLogger.debug("Records about bond payment calendar successfully received") 3814 3815 return calendar 3816 3817 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3818 """ 3819 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3820 pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, 3821 coupon yields, current yields and some statistics etc. 3822 3823 WARNING! This is too long operation if a lot of bonds requested from broker server. 3824 3825 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3826 3827 :param instruments: list of strings with tickers or FIGIs. 3828 :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3829 for further used by data scientists or stock analytics. 3830 :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. 3831 In XLSX-file and pandas dataframe fields mean: 3832 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3833 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3834 """ 3835 if instruments is None or not instruments: 3836 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3837 raise Exception("Ticker or FIGI required") 3838 3839 if isinstance(instruments, str): 3840 instruments = [instruments] 3841 3842 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3843 3844 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3845 3846 iCount = len(uniqueInstruments) 3847 tooLong = iCount >= 20 3848 if tooLong: 3849 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3850 3851 bonds = None 3852 for i, self.figi in enumerate(uniqueInstruments): 3853 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3854 3855 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3856 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3857 rawBond = self.SearchByFIGI(requestPrice=True) 3858 3859 # Widen raw data with UTC current time (iData["actualDateTime"]): 3860 actualDate = datetime.now(tzutc()) 3861 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3862 3863 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3864 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3865 3866 # Replace some values with human-readable: 3867 iData["nominalCurrency"] = iData["nominal"]["currency"] 3868 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3869 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3870 iData["aciCurrency"] = iData["aciValue"]["currency"] 3871 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3872 iData["issueSize"] = int(iData["issueSize"]) 3873 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3874 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3875 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3876 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3877 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3878 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3879 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3880 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3881 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3882 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3883 3884 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3885 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3886 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3887 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3888 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3889 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3890 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3891 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3892 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3893 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3894 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3895 3896 # Widen raw data with calendar data from `rawCalendar` values: 3897 calendarData = [] 3898 for item in iData["rawCalendar"]["events"]: 3899 calendarData.append({ 3900 "couponDate": item["couponDate"], 3901 "couponNumber": int(item["couponNumber"]), 3902 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3903 "payCurrency": item["payOneBond"]["currency"], 3904 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3905 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3906 "couponStartDate": item["couponStartDate"], 3907 "couponEndDate": item["couponEndDate"], 3908 "couponPeriod": item["couponPeriod"], 3909 }) 3910 3911 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3912 if "maturityDate" not in iData.keys(): 3913 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3914 3915 # Widen raw data with Coupon Rate. 3916 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3917 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3918 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3919 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3920 3921 # Widen raw data with Yield to Maturity (YTM) on current date. 3922 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3923 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3924 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3925 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3926 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3927 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3928 3929 iData["calendar"] = calendarData # adds calendar at the end 3930 3931 # Remove not used data: 3932 iData.pop("uid") 3933 iData.pop("positionUid") 3934 iData.pop("currentPrice") 3935 iData.pop("rawCalendar") 3936 3937 colNames = list(iData.keys()) 3938 if bonds is None: 3939 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3940 3941 else: 3942 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3943 3944 else: 3945 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3946 3947 processed = round(100 * (i + 1) / iCount, 1) 3948 if tooLong and processed % 5 == 0: 3949 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3950 3951 else: 3952 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3953 3954 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3955 3956 # Saving bonds from pandas dataframe to XLSX sheet: 3957 if xlsx and self.bondsXLSXFile: 3958 with pd.ExcelWriter( 3959 path=self.bondsXLSXFile, 3960 date_format=TKS_DATE_FORMAT, 3961 datetime_format=TKS_DATE_TIME_FORMAT, 3962 mode="w", 3963 ) as writer: 3964 bonds.to_excel( 3965 writer, 3966 sheet_name="Extended bonds data", 3967 index=True, 3968 encoding="UTF-8", 3969 freeze_panes=(1, 1), 3970 ) # saving as XLSX-file with freeze first row and column as headers 3971 3972 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3973 3974 return bonds 3975 3976 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3977 """ 3978 Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default. 3979 3980 WARNING! This is too long operation if a lot of bonds requested from broker server. 3981 3982 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3983 3984 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 3985 extended information about bonds: main info, current prices, bond payment calendar, 3986 coupon yields, current yields and some statistics etc. 3987 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3988 :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3989 for further used by data scientists or stock analytics. 3990 :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3991 """ 3992 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3993 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3994 3995 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3996 3997 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3998 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3999 calendar = None 4000 for bond in extBonds.iterrows(): 4001 for item in bond[1]["calendar"]: 4002 cData = { 4003 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4004 "couponDate": item["couponDate"], 4005 "figi": bond[1]["figi"], 4006 "ticker": bond[1]["ticker"], 4007 "name": bond[1]["name"], 4008 "couponNumber": item["couponNumber"], 4009 "payOneBond": item["payOneBond"], 4010 "payCurrency": item["payCurrency"], 4011 "couponType": item["couponType"], 4012 "couponPeriod": item["couponPeriod"], 4013 "fixDate": item["fixDate"], 4014 "couponStartDate": item["couponStartDate"], 4015 "couponEndDate": item["couponEndDate"], 4016 } 4017 4018 if calendar is None: 4019 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4020 4021 else: 4022 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4023 4024 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4025 4026 # Saving calendar from pandas dataframe to XLSX sheet: 4027 if xlsx: 4028 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4029 4030 with pd.ExcelWriter( 4031 path=xlsxCalendarFile, 4032 date_format=TKS_DATE_FORMAT, 4033 datetime_format=TKS_DATE_TIME_FORMAT, 4034 mode="w", 4035 ) as writer: 4036 humanReadable = calendar.copy(deep=True) 4037 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4038 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4039 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4040 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4041 humanReadable.columns = colNames # human-readable column names 4042 4043 humanReadable.to_excel( 4044 writer, 4045 sheet_name="Bond payments calendar", 4046 index=False, 4047 encoding="UTF-8", 4048 freeze_panes=(1, 2), 4049 ) # saving as XLSX-file with freeze first row and column as headers 4050 4051 del humanReadable # release df in memory 4052 4053 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4054 4055 return calendar 4056 4057 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4058 """ 4059 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4060 Also, creates Markdown file with calendar data, `calendar.md` by default. 4061 4062 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4063 4064 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 4065 extended information about bonds: main info, current prices, bond payment calendar, 4066 coupon yields, current yields and some statistics etc. 4067 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4068 :param show: if `True` then also printing bonds payment calendar to the console, 4069 otherwise save to file `calendarFile` only. `False` by default. 4070 :return: multilines text in Markdown format with bonds payment calendar as a table. 4071 """ 4072 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4073 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4074 4075 infoText = "# Bond payments calendar\n\n" 4076 4077 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate pandas dataframe with full calendar data 4078 4079 if not calendar.empty: 4080 splitLine = "| | | | | | | | | |\n" 4081 4082 info = [ 4083 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4084 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4085 ] 4086 4087 newMonth = False 4088 notOneBond = calendar["figi"].nunique() > 1 4089 for i, bond in enumerate(calendar.iterrows()): 4090 if newMonth and notOneBond: 4091 info.append(splitLine) 4092 4093 info.append( 4094 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4095 " √" if bond[1]["paid"] else " —", 4096 bond[1]["couponDate"].split("T")[0], 4097 bond[1]["figi"], 4098 bond[1]["ticker"], 4099 bond[1]["couponNumber"], 4100 "{} {}".format( 4101 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4102 bond[1]["payCurrency"], 4103 ), 4104 bond[1]["couponType"], 4105 bond[1]["couponPeriod"], 4106 bond[1]["fixDate"].split("T")[0], 4107 ) 4108 ) 4109 4110 if i < len(calendar.values) - 1: 4111 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4112 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4113 newMonth = False if curDate.month == nextDate.month else True 4114 4115 else: 4116 newMonth = False 4117 4118 infoText += "".join(info) 4119 4120 if show: 4121 uLogger.info("{}".format(infoText)) 4122 4123 if self.calendarFile is not None: 4124 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4125 fH.write(infoText) 4126 4127 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4128 4129 else: 4130 infoText += "No data\n" 4131 4132 return infoText 4133 4134 def OverviewAccounts(self, show: bool = False) -> dict: 4135 """ 4136 Method for parsing and show simple table with all available user accounts. 4137 4138 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4139 4140 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4141 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4142 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4143 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4144 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4145 "closed": "—", "access": "Full access" }, ...}}` 4146 """ 4147 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4148 4149 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4150 accounts = { 4151 item["id"]: { 4152 "type": TKS_ACCOUNT_TYPES[item["type"]], 4153 "name": item["name"], 4154 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4155 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4156 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4157 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4158 } for item in rawAccounts["accounts"] 4159 } 4160 4161 # Raw and parsed data with some fields replaced in "stat" section: 4162 view = { 4163 "rawAccounts": rawAccounts, 4164 "stat": accounts, 4165 } 4166 4167 # --- Prepare simple text table with only accounts data in human-readable format: 4168 if show: 4169 info = [ 4170 "# User accounts\n\n", 4171 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4172 "| Account ID | Type | Status | Name |\n", 4173 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4174 ] 4175 4176 for account in view["stat"].keys(): 4177 info.extend([ 4178 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4179 account, 4180 view["stat"][account]["type"], 4181 view["stat"][account]["status"], 4182 view["stat"][account]["name"], 4183 ) 4184 ]) 4185 4186 infoText = "".join(info) 4187 4188 uLogger.info(infoText) 4189 4190 if self.userAccountsFile: 4191 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4192 fH.write(infoText) 4193 4194 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4195 4196 return view 4197 4198 def OverviewUserInfo(self, show: bool = False) -> dict: 4199 """ 4200 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4201 4202 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4203 4204 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4205 :return: dict with raw parsed data from server and some calculated statistics about it. 4206 """ 4207 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4208 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4209 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4210 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4211 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4212 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4213 4214 # This is dict with parsed common user data: 4215 userInfo = { 4216 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4217 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4218 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4219 "tariff": rawUserInfo["tariff"], 4220 } 4221 4222 # This is an array of dict with parsed margin statuses for every account IDs: 4223 margins = {} 4224 for accountId in accounts.keys(): 4225 if rawMargins[accountId]: 4226 margins[accountId] = { 4227 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4228 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4229 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4230 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4231 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4232 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4233 } 4234 4235 else: 4236 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4237 4238 unary = {} # unary-connection limits 4239 for item in rawTariffLimits["unaryLimits"]: 4240 if item["limitPerMinute"] in unary.keys(): 4241 unary[item["limitPerMinute"]].extend(item["methods"]) 4242 4243 else: 4244 unary[item["limitPerMinute"]] = item["methods"] 4245 4246 stream = {} # stream-connection limits 4247 for item in rawTariffLimits["streamLimits"]: 4248 if item["limit"] in stream.keys(): 4249 stream[item["limit"]].extend(item["streams"]) 4250 4251 else: 4252 stream[item["limit"]] = item["streams"] 4253 4254 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4255 limits = { 4256 "unary": unary, 4257 "stream": stream, 4258 } 4259 4260 # Raw and parsed data as an output result: 4261 view = { 4262 "rawUserInfo": rawUserInfo, 4263 "rawAccounts": rawAccounts, 4264 "rawMargins": rawMargins, 4265 "rawTariffLimits": rawTariffLimits, 4266 "stat": { 4267 "userInfo": userInfo, 4268 "accounts": accounts, 4269 "margins": margins, 4270 "limits": limits, 4271 }, 4272 } 4273 4274 # --- Prepare text table with user information in human-readable format: 4275 if show: 4276 info = [ 4277 "# Full user information\n\n", 4278 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4279 "## Common information\n\n", 4280 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4281 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4282 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4283 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4284 "\n## User accounts\n\n", 4285 ] 4286 4287 for account in view["stat"]["accounts"].keys(): 4288 info.extend([ 4289 "### ID: [{}]\n\n".format(account), 4290 "| Parameters | Values |\n", 4291 "|----------------------|--------------------------------------------------------------|\n", 4292 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4293 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4294 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4295 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4296 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4297 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4298 ]) 4299 4300 if margins[account]: 4301 info.extend([ 4302 "| Margin status: | Enabled |\n", 4303 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4304 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4305 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4306 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4307 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4308 ]) 4309 4310 else: 4311 info.append("| Margin status: | Disabled |\n\n") 4312 4313 info.extend([ 4314 "\n## Current user tariff limits\n", 4315 "\nSee also:\n", 4316 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4317 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4318 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4319 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4320 "\n### Unary limits\n", 4321 ]) 4322 4323 if unary: 4324 for key, values in sorted(unary.items()): 4325 info.append("\n* Max requests per minute: {}\n".format(key)) 4326 4327 for value in values: 4328 info.append(" - {}\n".format(value)) 4329 4330 else: 4331 info.append("\nNot available\n") 4332 4333 info.append("\n### Stream limits\n") 4334 4335 if stream: 4336 for key, values in sorted(stream.items()): 4337 info.append("\n* Max stream connections: {}\n".format(key)) 4338 4339 for value in values: 4340 info.append(" - {}\n".format(value)) 4341 4342 else: 4343 info.append("\nNot available\n") 4344 4345 infoText = "".join(info) 4346 4347 uLogger.info(infoText) 4348 4349 if self.userInfoFile: 4350 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4351 fH.write(infoText) 4352 4353 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4354 4355 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
196 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 197 """ 198 Main class init. 199 200 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 201 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 202 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 203 :param useCache: use default cache file with raw data to use instead of `iList`. 204 True by default. Cache is auto-update if new day has come. 205 If you don't want to use cache and always updates raw data then set `useCache=False`. 206 :param defaultCache: path to default cache file. `dump.json` by default. 207 """ 208 if token is None or not token: 209 try: 210 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 211 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 212 213 except KeyError: 214 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 215 raise Exception("Token required") 216 217 else: 218 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 219 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 220 221 if accountId is None or not accountId: 222 try: 223 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 224 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 225 226 except KeyError: 227 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 228 229 else: 230 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 231 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 232 233 self.version = __version__ # duplicate here used TKSBrokerAPI main version 234 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 235 236 Latest version: https://pypi.org/project/tksbrokerapi/ 237 """ 238 239 self.aliases = TKS_TICKER_ALIASES 240 """Some aliases instead official tickers. 241 242 See also: `TKSEnums.TKS_TICKER_ALIASES` 243 """ 244 245 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 246 247 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 248 249 self.ticker = "" 250 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 251 252 See also: `SearchByTicker()`, `SearchInstruments()`. 253 """ 254 255 self.figi = "" 256 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 257 258 See also: `SearchByFIGI()`, `SearchInstruments()`. 259 """ 260 261 self.depth = 1 262 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 263 264 See also: `GetCurrentPrices()`. 265 """ 266 267 self.server = r"https://invest-public-api.tinkoff.ru/rest" 268 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 269 270 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 271 """ 272 273 uLogger.debug("Broker API server: {}".format(self.server)) 274 275 self.timeout = 15 276 """Server operations timeout in seconds. Default: `15`. 277 278 See also: `SendAPIRequest()`. 279 """ 280 281 self.headers = { 282 "Content-Type": "application/json", 283 "accept": "application/json", 284 "Authorization": "Bearer {}".format(self.token), 285 "x-app-name": "Tim55667757.TKSBrokerAPI", 286 } 287 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 288 289 See also: `SendAPIRequest()`. 290 """ 291 292 self.body = None 293 """Request body which send to broker server. Default: `None`. 294 295 See also: `SendAPIRequest()`. 296 """ 297 298 self.historyFile = None 299 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe. 300 301 See also: `History()`. 302 """ 303 304 self.htmlHistoryFile = "index.html" 305 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 306 307 See also: `ShowHistoryChart()`. 308 """ 309 310 self.instrumentsFile = "instruments.md" 311 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 312 313 See also: `ShowInstrumentsInfo()`. 314 """ 315 316 self.searchResultsFile = "search-results.md" 317 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 318 319 See also: `SearchInstruments()`. 320 """ 321 322 self.pricesFile = "prices.md" 323 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 324 325 See also: `GetListOfPrices()`. 326 """ 327 328 self.infoFile = "info.md" 329 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 330 331 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 332 """ 333 334 self.bondsXLSXFile = "ext-bonds.xlsx" 335 """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 336 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 337 338 See also: `ExtendBondsData()`. 339 """ 340 341 self.calendarFile = "calendar.md" 342 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 343 344 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 345 346 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 347 """ 348 349 self.overviewFile = "overview.md" 350 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 351 352 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 353 """ 354 355 self.overviewDigestFile = "overview-digest.md" 356 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 357 358 See also: `Overview()` with parameter `details="digest"`. 359 """ 360 361 self.overviewPositionsFile = "overview-positions.md" 362 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 363 364 See also: `Overview()` with parameter `details="positions"`. 365 """ 366 367 self.overviewOrdersFile = "overview-orders.md" 368 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 369 370 See also: `Overview()` with parameter `details="orders"`. 371 """ 372 373 self.overviewAnalyticsFile = "overview-analytics.md" 374 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 375 376 See also: `Overview()` with parameter `details="analytics"`. 377 """ 378 379 self.reportFile = "deals.md" 380 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 381 382 See also: `Deals()`. 383 """ 384 385 self.withdrawalLimitsFile = "limits.md" 386 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 387 388 See also: `OverviewLimits()` and `RequestLimits()`. 389 """ 390 391 self.userInfoFile = "user-info.md" 392 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 393 394 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 395 """ 396 397 self.userAccountsFile = "accounts.md" 398 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 399 400 See also: `OverviewAccounts()`, `RequestAccounts()`. 401 """ 402 403 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 404 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 405 406 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 407 408 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 409 """ 410 411 self.iList = None # init iList for raw instruments data 412 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 413 414 See also: `Listing()`, `DumpInstruments()`. 415 """ 416 417 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 418 if useCache: 419 if os.path.exists(self.iListDumpFile): 420 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 421 curTime = datetime.now(tzutc()) 422 423 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 424 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 425 426 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 427 428 else: 429 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 430 431 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 432 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 433 434 else: 435 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 436 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 437 438 else: 439 self.iList = self.Listing() # request new raw instruments data from broker server 440 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 441 442 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 443 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 444 445 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 446 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only pandas dataframe.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider pandas dataframe with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
470 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 471 """ 472 Send GET or POST request to broker server and receive JSON object. 473 474 self.header: must be defining with dictionary of headers. 475 self.body: if define then used as request body. None by default. 476 self.timeout: global request timeout, 15 seconds by default. 477 :param url: url with REST request. 478 :param reqType: send "GET" or "POST" request. "GET" by default. 479 :param retry: how many times retry after first request if an 5xx server errors occurred. 480 :param pause: sleep time in seconds between retries. 481 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 482 :return: response JSON (dictionary) from broker. 483 """ 484 if reqType not in ("GET", "POST"): 485 uLogger.error("You can define request type: 'GET' or 'POST'!") 486 raise Exception("Incorrect value") 487 488 if debug: 489 uLogger.debug("Request parameters:") 490 uLogger.debug(" - REST API URL: {}".format(url)) 491 uLogger.debug(" - request type: {}".format(reqType)) 492 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 493 uLogger.debug(" - body: {}".format(self.body)) 494 495 # fast hack to avoid all operations with some tickers/FIGI 496 responseJSON = {} 497 oK = True 498 for item in self.exclude: 499 if item in url: 500 if debug: 501 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 502 503 oK = False 504 break 505 506 if oK: 507 counter = 0 508 response = None 509 errMsg = "" 510 511 while not response and counter <= retry: 512 if reqType == "GET": 513 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 514 515 if reqType == "POST": 516 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 517 518 if debug: 519 uLogger.debug("Response:") 520 uLogger.debug(" - status code: {}".format(response.status_code)) 521 uLogger.debug(" - reason: {}".format(response.reason)) 522 uLogger.debug(" - body length: {}".format(len(response.text))) 523 uLogger.debug(" - headers: {}".format(response.headers)) 524 525 # Server returns some headers: 526 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 527 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 528 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 529 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 530 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 531 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 532 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 533 sleep(rateLimitWait) 534 535 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 536 if 400 <= response.status_code < 500: 537 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 538 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 539 counter = retry + 1 540 541 if 500 <= response.status_code < 600: 542 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 543 uLogger.debug(" - not oK, {}".format(errMsg)) 544 counter += 1 545 546 if counter <= retry: 547 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 548 sleep(pause) 549 550 responseJSON = self._ParseJSON(response.text) 551 552 if errMsg: 553 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 554 uLogger.error(" - not oK, {}".format(errMsg)) 555 556 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
- debug: if
Truethen print more debug information, e.g. request and response parameters, headers etc.
Returns
response JSON (dictionary) from broker.
589 def Listing(self) -> dict: 590 """ 591 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 592 593 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 594 """ 595 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 596 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 597 598 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 599 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 600 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 601 602 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 603 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 604 poolUpdater.close() 605 606 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 607 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 608 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 609 610 # calculate minimum price increment (step) for all instruments and set up instrument's type: 611 for iType in iList.keys(): 612 for ticker in iList[iType]: 613 iList[iType][ticker]["type"] = iType 614 615 if "minPriceIncrement" in iList[iType][ticker].keys(): 616 iList[iType][ticker]["step"] = NanoToFloat( 617 iList[iType][ticker]["minPriceIncrement"]["units"], 618 iList[iType][ticker]["minPriceIncrement"]["nano"], 619 ) 620 621 else: 622 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 623 624 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
626 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 627 """ 628 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 629 630 See also: `DumpInstruments()`, `Listing()`. 631 632 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 633 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 634 """ 635 if self.iListDumpFile is None or not self.iListDumpFile: 636 uLogger.error("Output name of dump file must be defined!") 637 raise Exception("Filename required") 638 639 if not self.iList or forceUpdate: 640 self.iList = self.Listing() 641 642 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 643 644 # Save as XLSX with separated sheets for every type of instruments: 645 with pd.ExcelWriter( 646 path=xlsxDumpFile, 647 date_format=TKS_DATE_FORMAT, 648 datetime_format=TKS_DATE_TIME_FORMAT, 649 mode="w", 650 ) as writer: 651 for iType in TKS_INSTRUMENTS: 652 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 653 df = df[sorted(df)] # sorted by column names 654 df = df.applymap( 655 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 656 na_action="ignore", 657 ) # converting numbers from nano-type to float in every cell 658 df.to_excel( 659 writer, 660 sheet_name=iType, 661 encoding="UTF-8", 662 freeze_panes=(1, 1), 663 ) # saving as XLSX-file with freeze first row and column as headers 664 665 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
667 def DumpInstruments(self, forceUpdate: bool = True) -> str: 668 """ 669 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 670 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 671 672 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 673 674 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 675 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 676 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 677 """ 678 if self.iListDumpFile is None or not self.iListDumpFile: 679 uLogger.error("Output name of dump file must be defined!") 680 raise Exception("Filename required") 681 682 if not self.iList or forceUpdate: 683 self.iList = self.Listing() 684 685 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 686 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 687 fH.write(jsonDump) 688 689 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 690 691 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
693 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 694 """ 695 Show information about one instrument defined by json data and prints it in Markdown format. 696 697 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 698 699 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 700 :param show: if `True` then also printing information about instrument and its current price. 701 :return: multilines text in Markdown format with information about one instrument. 702 """ 703 splitLine = "| | |\n" 704 infoText = "" 705 706 if iJSON is not None and iJSON and isinstance(iJSON, dict): 707 info = [ 708 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 709 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 710 "| Parameters | Values |\n", 711 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 712 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 713 "| Full name: | {:<54} |\n".format(iJSON["name"]), 714 ] 715 716 if "sector" in iJSON.keys() and iJSON["sector"]: 717 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 718 719 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 720 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 721 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 722 ))) 723 724 info.extend([ 725 splitLine, 726 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 727 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 728 ]) 729 730 if "isin" in iJSON.keys() and iJSON["isin"]: 731 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 732 733 if "classCode" in iJSON.keys(): 734 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 735 736 info.extend([ 737 splitLine, 738 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 739 splitLine, 740 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 741 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 742 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 743 ]) 744 745 if iJSON["figi"]: 746 self.figi = iJSON["figi"] 747 iJSON = iJSON | self.RequestTradingStatus() 748 749 info.extend([ 750 splitLine, 751 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 752 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 753 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 754 ]) 755 756 info.append(splitLine) 757 758 if "type" in iJSON.keys() and iJSON["type"]: 759 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 760 761 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 762 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 763 764 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 765 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 766 767 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 768 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 769 770 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 771 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 772 773 if "focusType" in iJSON.keys() and iJSON["focusType"]: 774 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 775 776 if "assetType" in iJSON.keys() and iJSON["assetType"]: 777 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 778 779 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 780 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 781 782 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 783 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 784 785 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 786 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 787 788 if "currency" in iJSON.keys(): 789 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 790 791 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 792 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 793 794 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 795 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 796 797 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 798 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 799 800 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 801 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 802 803 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 804 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 805 806 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 807 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 808 809 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 810 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 811 812 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 813 info.append("| Perpetual bond: | Yes |\n") 814 815 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 816 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 817 818 iExt = None 819 if iJSON["type"] == "Bonds": 820 info.extend([ 821 splitLine, 822 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 823 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 824 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 825 iJSON["nominal"]["currency"], 826 )), 827 ]) 828 829 if "floatingCouponFlag" in iJSON.keys(): 830 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 831 832 if "amortizationFlag" in iJSON.keys(): 833 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 834 835 info.append(splitLine) 836 837 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 838 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 839 840 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 841 842 info.extend([ 843 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 844 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 845 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 846 ]) 847 848 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 849 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 850 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 851 iJSON["aciValue"]["currency"] 852 ))) 853 854 if "currentPrice" in iJSON.keys(): 855 info.append(splitLine) 856 857 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 858 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 859 860 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 861 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 862 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 863 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 864 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 865 866 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 867 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 868 869 info.extend([ 870 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 871 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 872 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 873 )), 874 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 875 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 876 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 877 )), 878 "| Changes between last deal price and last close | {:<54} |\n".format( 879 "{:.2f}%{}".format( 880 iJSON["currentPrice"]["changes"], 881 " ({}{:.2f} {})".format( 882 "+" if bondChangesDelta > 0 else "", 883 bondChangesDelta, 884 aciCurrency 885 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 886 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 887 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 888 currency 889 ), 890 ) 891 ), 892 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 893 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 894 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 895 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 898 )), 899 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 900 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 901 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 902 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 904 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 905 )), 906 ]) 907 908 if "lot" in iJSON.keys(): 909 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 910 911 if "step" in iJSON.keys() and iJSON["step"] != 0: 912 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 913 914 # Add bond payment calendar: 915 if iJSON["type"] == "Bonds": 916 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 917 info.extend(["\n", strCalendar]) 918 919 infoText += "".join(info) 920 921 if show: 922 uLogger.info("{}".format(infoText)) 923 924 else: 925 uLogger.debug("{}".format(infoText)) 926 927 if self.infoFile is not None: 928 with open(self.infoFile, "w", encoding="UTF-8") as fH: 929 fH.write(infoText) 930 931 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 932 933 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
935 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 936 """ 937 Search and return raw broker's information about instrument by its ticker. 938 `ticker` must be defined! If debug=True then print all debug messages. 939 940 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 941 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 942 :param debug: if `True` then print all debug console messages. 943 :return: JSON formatted data with information about instrument. 944 """ 945 tickerJSON = {} 946 if debug: 947 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 948 949 if not self.ticker: 950 uLogger.warning("self.ticker variable is not be empty!") 951 952 else: 953 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 954 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 955 raise Exception("Instrument not allowed") 956 957 if not self.iList: 958 self.iList = self.Listing() 959 960 if self.ticker in self.iList["Shares"].keys(): 961 tickerJSON = self.iList["Shares"][self.ticker] 962 if debug: 963 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 964 965 elif self.ticker in self.iList["Currencies"].keys(): 966 tickerJSON = self.iList["Currencies"][self.ticker] 967 if debug: 968 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 969 970 elif self.ticker in self.iList["Bonds"].keys(): 971 tickerJSON = self.iList["Bonds"][self.ticker] 972 if debug: 973 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 974 975 elif self.ticker in self.iList["Etfs"].keys(): 976 tickerJSON = self.iList["Etfs"][self.ticker] 977 if debug: 978 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 979 980 elif self.ticker in self.iList["Futures"].keys(): 981 tickerJSON = self.iList["Futures"][self.ticker] 982 if debug: 983 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 984 985 if tickerJSON: 986 self.figi = tickerJSON["figi"] 987 988 if requestPrice: 989 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 990 991 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 992 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 993 994 else: 995 tickerJSON["currentPrice"]["changes"] = 0 996 997 if show: 998 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 999 1000 else: 1001 if show: 1002 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1003 1004 return tickerJSON
Search and return raw broker's information about instrument by its ticker.
ticker must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1006 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1007 """ 1008 Search and return raw broker's information about instrument by its FIGI. 1009 `figi` must be defined! If debug=True then print all debug messages. 1010 1011 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1012 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1013 :param debug: if `True` then print all debug console messages. 1014 :return: JSON formatted data with information about instrument. 1015 """ 1016 figiJSON = {} 1017 if debug: 1018 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1019 1020 if not self.figi: 1021 uLogger.warning("self.figi variable is not be empty!") 1022 1023 else: 1024 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1025 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1026 raise Exception("Instrument not allowed") 1027 1028 if not self.iList: 1029 self.iList = self.Listing() 1030 1031 for item in self.iList["Shares"].keys(): 1032 if self.figi == self.iList["Shares"][item]["figi"]: 1033 figiJSON = self.iList["Shares"][item] 1034 1035 if debug: 1036 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1037 1038 break 1039 1040 if not figiJSON: 1041 for item in self.iList["Currencies"].keys(): 1042 if self.figi == self.iList["Currencies"][item]["figi"]: 1043 figiJSON = self.iList["Currencies"][item] 1044 1045 if debug: 1046 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1047 1048 break 1049 1050 if not figiJSON: 1051 for item in self.iList["Bonds"].keys(): 1052 if self.figi == self.iList["Bonds"][item]["figi"]: 1053 figiJSON = self.iList["Bonds"][item] 1054 1055 if debug: 1056 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1057 1058 break 1059 1060 if not figiJSON: 1061 for item in self.iList["Etfs"].keys(): 1062 if self.figi == self.iList["Etfs"][item]["figi"]: 1063 figiJSON = self.iList["Etfs"][item] 1064 1065 if debug: 1066 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1067 1068 break 1069 1070 if not figiJSON: 1071 for item in self.iList["Futures"].keys(): 1072 if self.figi == self.iList["Futures"][item]["figi"]: 1073 figiJSON = self.iList["Futures"][item] 1074 1075 if debug: 1076 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1077 1078 break 1079 1080 if figiJSON: 1081 self.figi = figiJSON["figi"] 1082 self.ticker = figiJSON["ticker"] 1083 1084 if requestPrice: 1085 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1086 1087 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1088 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1089 1090 else: 1091 figiJSON["currentPrice"]["changes"] = 0 1092 1093 if show: 1094 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1095 1096 else: 1097 if show: 1098 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1099 1100 return figiJSON
Search and return raw broker's information about instrument by its FIGI.
figi must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1102 def GetCurrentPrices(self, show: bool = True) -> dict: 1103 """ 1104 Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: 1105 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1106 1107 See also: 1108 1109 :param show: if `True` then print DOM to log and console. 1110 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1111 """ 1112 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1113 1114 if self.depth < 1: 1115 uLogger.error("Depth of Market (DOM) must be >=1!") 1116 raise Exception("Incorrect value") 1117 1118 if not (self.ticker or self.figi): 1119 uLogger.error("self.ticker or self.figi variables must be defined!") 1120 raise Exception("Ticker or FIGI required") 1121 1122 if self.ticker and not self.figi: 1123 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1124 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1125 1126 if not self.ticker and self.figi: 1127 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1128 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1129 1130 if not self.figi: 1131 uLogger.error("FIGI is not defined!") 1132 raise Exception("Ticker or FIGI required") 1133 1134 else: 1135 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1136 1137 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1138 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1139 self.body = str({"figi": self.figi, "depth": self.depth}) 1140 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") 1141 1142 if pricesResponse: 1143 # list of dicts with sellers orders: 1144 prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1145 1146 # list of dicts with buyers orders: 1147 prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1148 1149 # max price of instrument at this time: 1150 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1151 1152 # min price of instrument at this time: 1153 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1154 1155 # last price of deal with instrument: 1156 prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0 1157 1158 # last close price of instrument: 1159 prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0 1160 1161 else: 1162 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1163 uLogger.debug("Server response: {}".format(pricesResponse)) 1164 1165 if show: 1166 if prices["buy"] or prices["sell"]: 1167 info = [ 1168 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1169 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1170 self.ticker, 1171 self.figi, 1172 self.depth, 1173 ), 1174 uLog.sepShort, "\n", 1175 " Orders of Buyers | Orders of Sellers\n", 1176 uLog.sepShort, "\n", 1177 " Sell prices (vol.) | Buy prices (vol.)\n", 1178 uLog.sepShort, "\n", 1179 ] 1180 1181 if not prices["buy"]: 1182 info.append(" | No orders!\n") 1183 sumBuy = 0 1184 1185 else: 1186 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1187 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1188 for item in maxMinSorted: 1189 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1190 1191 if not prices["sell"]: 1192 info.append("No orders! |\n") 1193 sumSell = 0 1194 1195 else: 1196 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1197 for item in prices["sell"]: 1198 info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1199 1200 info.extend([ 1201 uLog.sepShort, "\n", 1202 "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1203 uLog.sepShort, "\n", 1204 ]) 1205 1206 infoText = "".join(info) 1207 1208 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1209 1210 else: 1211 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1212 1213 return prices
Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
See also:
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}.
1215 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1216 """ 1217 This method get and show information about all available broker instruments for current user account. 1218 If `instrumentsFile` string is not empty then also save information to this file. 1219 1220 :param show: if `True` then print results to console, if `False` - print only to file. 1221 :return: multi-lines string with all available broker instruments 1222 """ 1223 if not self.iList: 1224 self.iList = self.Listing() 1225 1226 info = [ 1227 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1228 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1229 ] 1230 1231 # add instruments count by type: 1232 for iType in self.iList.keys(): 1233 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1234 1235 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1236 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1237 1238 # generating info tables with all instruments by type: 1239 for iType in self.iList.keys(): 1240 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1241 1242 for instrument in self.iList[iType].keys(): 1243 iName = self.iList[iType][instrument]["name"] # instrument's name 1244 if len(iName) > 57: 1245 iName = "{}...".format(iName[:54]) # right trim for a long string 1246 1247 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1248 self.iList[iType][instrument]["ticker"], 1249 iName, 1250 self.iList[iType][instrument]["figi"], 1251 self.iList[iType][instrument]["currency"], 1252 self.iList[iType][instrument]["lot"], 1253 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1254 )) 1255 1256 infoText = "".join(info) 1257 1258 if show: 1259 uLogger.info(infoText) 1260 1261 if self.instrumentsFile: 1262 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1263 fH.write(infoText) 1264 1265 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1266 1267 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse- print only to file.
Returns
multi-lines string with all available broker instruments
1269 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1270 """ 1271 This method search and show information about instruments by part of its ticker, FIGI or name. 1272 If `searchResultsFile` string is not empty then also save information to this file. 1273 1274 :param pattern: string with part of ticker, FIGI or instrument's name. 1275 :param show: if `True` then print results to console, if `False` - return list of result only. 1276 :return: list of dictionaries with all found instruments. 1277 """ 1278 if not self.iList: 1279 self.iList = self.Listing() 1280 1281 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1282 compiledPattern = re.compile(pattern, re.IGNORECASE) 1283 1284 for iType in self.iList: 1285 for instrument in self.iList[iType].values(): 1286 searchResult = compiledPattern.search(" ".join( 1287 [instrument["ticker"], instrument["figi"], instrument["name"]] 1288 )) 1289 1290 if searchResult: 1291 searchResults[iType][instrument["ticker"]] = instrument 1292 1293 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1294 info = [ 1295 "# Search results\n\n", 1296 "* **Search pattern:** [{}]\n".format(pattern), 1297 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1298 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1299 ] 1300 infoShort = info[:] 1301 1302 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1303 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1304 skippedLine = "| ... | ... | ... | ... |\n" 1305 1306 if resultsLen == 0: 1307 info.append("\nNo results\n") 1308 infoShort.append("\nNo results\n") 1309 uLogger.warning("No results. Try changing your search pattern.") 1310 1311 else: 1312 for iType in searchResults: 1313 iTypeValuesCount = len(searchResults[iType].values()) 1314 if iTypeValuesCount > 0: 1315 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1316 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1317 1318 for instrument in searchResults[iType].values(): 1319 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1320 instrument["type"], 1321 instrument["ticker"], 1322 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1323 instrument["figi"], 1324 )) 1325 1326 if iTypeValuesCount <= 5: 1327 infoShort.extend(info[-iTypeValuesCount:]) 1328 1329 else: 1330 infoShort.extend(info[-5:]) 1331 infoShort.append(skippedLine) 1332 1333 infoText = "".join(info) 1334 infoTextShort = "".join(infoShort) 1335 1336 if show: 1337 uLogger.info(infoTextShort) 1338 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1339 1340 if self.searchResultsFile: 1341 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1342 fH.write(infoText) 1343 1344 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1345 1346 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse- return list of result only.
Returns
list of dictionaries with all found instruments.
1348 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1349 """ 1350 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1351 1352 :param instruments: list of strings with tickers or FIGIs. 1353 :return: list with unique instrument FIGIs only. 1354 """ 1355 requestedInstruments = [] 1356 for iName in instruments: 1357 if iName not in self.aliases.keys(): 1358 if iName not in requestedInstruments: 1359 requestedInstruments.append(iName) 1360 1361 else: 1362 if iName not in requestedInstruments: 1363 if self.aliases[iName] not in requestedInstruments: 1364 requestedInstruments.append(self.aliases[iName]) 1365 1366 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1367 1368 onlyUniqueFIGIs = [] 1369 for iName in requestedInstruments: 1370 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1371 continue 1372 1373 self.ticker = iName 1374 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1375 1376 if not iData: 1377 self.ticker = "" 1378 self.figi = iName 1379 1380 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1381 1382 if not iData: 1383 self.figi = "" 1384 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1385 1386 if iData and iData["figi"] not in onlyUniqueFIGIs: 1387 onlyUniqueFIGIs.append(iData["figi"]) 1388 1389 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1390 1391 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1393 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1394 """ 1395 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1396 See limits: https://tinkoff.github.io/investAPI/limits/ 1397 If `pricesFile` string is not empty then also save information to this file. 1398 1399 :param instruments: list of strings with tickers or FIGIs. 1400 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1401 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1402 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1403 """ 1404 if instruments is None or not instruments: 1405 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1406 raise Exception("Ticker or FIGI required") 1407 1408 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1409 1410 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1411 1412 iList = [] # trying to get info and current prices about all unique instruments: 1413 for self.figi in onlyUniqueFIGIs: 1414 iData = self.SearchByFIGI(requestPrice=True) 1415 iList.append(iData) 1416 1417 self.ShowListOfPrices(iList, show) 1418 1419 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1421 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1422 """ 1423 Show table contains current prices of given instruments. 1424 1425 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1427 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1428 :return: multilines text in Markdown format as a table contains current prices. 1429 """ 1430 infoText = "" 1431 1432 if show or self.pricesFile: 1433 info = [ 1434 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1435 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1436 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1437 ] 1438 1439 for item in iList: 1440 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1441 item["ticker"], 1442 item["figi"], 1443 item["type"], 1444 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1445 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1446 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1447 "{} / {}".format( 1448 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1449 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1450 ), 1451 "{} / {}".format( 1452 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1453 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1454 ), 1455 item["currency"], 1456 )) 1457 1458 infoText = "".join(info) 1459 1460 if show: 1461 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1462 1463 if self.pricesFile: 1464 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1465 fH.write(infoText) 1466 1467 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1468 1469 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1471 def RequestTradingStatus(self) -> dict: 1472 """ 1473 Requesting trading status for the instrument defined by `figi` variable. 1474 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1475 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1476 1477 :return: dictionary with trading status attributes. Response example: 1478 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1479 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1480 """ 1481 if self.figi is None or not self.figi: 1482 uLogger.error("Variable `figi` must be defined for using this method!") 1483 raise Exception("FIGI required") 1484 1485 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1486 1487 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1488 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1489 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1490 1491 uLogger.debug("Records about current trading status successfully received") 1492 1493 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1495 def RequestPortfolio(self) -> dict: 1496 """ 1497 Requesting actual user's portfolio for current `accountId`. 1498 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1499 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1500 1501 :return: dictionary with user's portfolio. 1502 """ 1503 if self.accountId is None or not self.accountId: 1504 uLogger.error("Variable `accountId` must be defined for using this method!") 1505 raise Exception("Account ID required") 1506 1507 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1508 1509 self.body = str({"accountId": self.accountId}) 1510 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1511 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1512 1513 uLogger.debug("Records about user's portfolio successfully received") 1514 1515 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1517 def RequestPositions(self) -> dict: 1518 """ 1519 Requesting open positions by currencies and instruments for current `accountId`. 1520 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1521 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1522 1523 :return: dictionary with open positions by instruments. 1524 """ 1525 if self.accountId is None or not self.accountId: 1526 uLogger.error("Variable `accountId` must be defined for using this method!") 1527 raise Exception("Account ID required") 1528 1529 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1530 1531 self.body = str({"accountId": self.accountId}) 1532 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1533 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1534 1535 uLogger.debug("Records about current open positions successfully received") 1536 1537 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1539 def RequestPendingOrders(self) -> list: 1540 """ 1541 Requesting current actual pending orders for current `accountId`. 1542 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1543 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1544 1545 :return: list of dictionaries with pending orders. 1546 """ 1547 if self.accountId is None or not self.accountId: 1548 uLogger.error("Variable `accountId` must be defined for using this method!") 1549 raise Exception("Account ID required") 1550 1551 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1552 1553 self.body = str({"accountId": self.accountId}) 1554 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1555 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1556 1557 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1558 1559 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1561 def RequestStopOrders(self) -> list: 1562 """ 1563 Requesting current actual stop orders for current `accountId`. 1564 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1565 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1566 1567 :return: list of dictionaries with stop orders. 1568 """ 1569 if self.accountId is None or not self.accountId: 1570 uLogger.error("Variable `accountId` must be defined for using this method!") 1571 raise Exception("Account ID required") 1572 1573 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1574 1575 self.body = str({"accountId": self.accountId}) 1576 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1577 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1578 1579 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1580 1581 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1583 def Overview(self, show: bool = False, details: str = "full") -> dict: 1584 """ 1585 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1586 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1587 are defined then also save information to file. 1588 1589 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1590 many requests about the state of the portfolio, and then, based on the received data, a large number 1591 of calculation and statistics are collected. 1592 1593 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1594 :param details: how detailed should the information be? You should specify one of strings: 1595 `full` - shows full available information about portfolio status (by default), 1596 `positions` - shows only open positions, 1597 `digest` - show a short digest of the portfolio status, 1598 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1599 `orders` - shows only sections of open limits and stop orders. 1600 :return: dictionary with client's raw portfolio and some statistics. 1601 """ 1602 if self.accountId is None or not self.accountId: 1603 uLogger.error("Variable `accountId` must be defined for using this method!") 1604 raise Exception("Account ID required") 1605 1606 view = { 1607 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1608 "headers": {}, # list of dictionaries, response headers without "positions" section 1609 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1610 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1611 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1612 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1613 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1614 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1615 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1616 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1617 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1618 }, 1619 "stat": { # --- some statistics calculated using "raw" sections: 1620 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1621 "availableRUB": 0., # available rubles (without other currencies) 1622 "blockedRUB": 0., # blocked sum in Russian Rouble 1623 "totalChangesRUB": 0., # changes for all open trades in RUB 1624 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1625 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1626 "sharesCostRUB": 0., # costs of all shares in RUB 1627 "bondsCostRUB": 0., # costs of all bonds in RUB 1628 "etfsCostRUB": 0., # costs of all etfs in RUB 1629 "futuresCostRUB": 0., # costs of all futures in RUB 1630 "Currencies": [], # list of dictionaries of all currencies statistics 1631 "Shares": [], # list of dictionaries of all shares statistics 1632 "Bonds": [], # list of dictionaries of all bonds statistics 1633 "Etfs": [], # list of dictionaries of all etfs statistics 1634 "Futures": [], # list of dictionaries of all futures statistics 1635 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1636 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1637 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1638 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1639 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1640 }, 1641 "analytics": { # --- some analytics of portfolio: 1642 "distrByAssets": {}, # portfolio distribution by assets 1643 "distrByCompanies": {}, # portfolio distribution by companies 1644 "distrBySectors": {}, # portfolio distribution by sectors 1645 "distrByCurrencies": {}, # portfolio distribution by currencies 1646 "distrByCountries": {}, # portfolio distribution by countries 1647 } 1648 } 1649 1650 details = details.lower() 1651 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1652 if details not in availableDetails: 1653 details = "full" 1654 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1655 1656 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1657 1658 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1659 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1660 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1661 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1662 1663 # save response headers without "positions" section: 1664 for key in portfolioResponse.keys(): 1665 if key != "positions": 1666 view["raw"]["headers"][key] = portfolioResponse[key] 1667 1668 else: 1669 continue 1670 1671 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1672 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1673 for item in portfolioResponse["positions"]: 1674 if item["instrumentType"] == "currency": 1675 self.figi = item["figi"] 1676 curr = self.SearchByFIGI(requestPrice=False) 1677 1678 # current price of currency in RUB: 1679 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1680 "name": curr["name"], 1681 "currentPrice": NanoToFloat( 1682 item["currentPrice"]["units"], 1683 item["currentPrice"]["nano"] 1684 ), 1685 } 1686 1687 view["raw"]["Currencies"].append(item) 1688 1689 elif item["instrumentType"] == "share": 1690 view["raw"]["Shares"].append(item) 1691 1692 elif item["instrumentType"] == "bond": 1693 view["raw"]["Bonds"].append(item) 1694 1695 elif item["instrumentType"] == "etf": 1696 view["raw"]["Etfs"].append(item) 1697 1698 elif item["instrumentType"] == "futures": 1699 view["raw"]["Futures"].append(item) 1700 1701 else: 1702 continue 1703 1704 # how many volume of currencies (by ISO currency name) are blocked: 1705 for item in view["raw"]["positions"]["blocked"]: 1706 blocked = NanoToFloat(item["units"], item["nano"]) 1707 if blocked > 0: 1708 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1709 1710 # how many volume of instruments (by FIGI) are blocked: 1711 for item in view["raw"]["positions"]["securities"]: 1712 blocked = int(item["blocked"]) 1713 if blocked > 0: 1714 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1715 1716 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1717 1718 if "rub" in allBlocked.keys(): 1719 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1720 1721 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1722 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1723 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1724 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1725 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1726 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1727 view["stat"]["portfolioCostRUB"] = sum([ 1728 view["stat"]["allCurrenciesCostRUB"], 1729 view["stat"]["sharesCostRUB"], 1730 view["stat"]["bondsCostRUB"], 1731 view["stat"]["etfsCostRUB"], 1732 view["stat"]["futuresCostRUB"], 1733 ]) 1734 1735 # --- calculating some portfolio statistics: 1736 byComp = {} # distribution by companies 1737 bySect = {} # distribution by sectors 1738 byCurr = {} # distribution by currencies (include RUB) 1739 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1740 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1741 1742 for item in portfolioResponse["positions"]: 1743 self.figi = item["figi"] 1744 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1745 1746 if instrument: 1747 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1748 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1749 1750 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1751 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1752 1753 else: 1754 blocked = 0 1755 1756 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1757 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1758 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1759 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1760 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1761 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1762 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1763 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1764 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1765 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1766 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1767 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1768 1769 statData = { 1770 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1771 "ticker": instrument["ticker"], # ticker by FIGI 1772 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1773 "volume": volume, # available volume of instrument 1774 "lots": lots, # volume in lots of instrument 1775 "direction": direction, # direction of an instrument's position: short or long 1776 "blocked": blocked, # blocked volume of currency or instrument 1777 "currentPrice": curPrice, # current instrument's price in basic asset 1778 "average": average, # current average position price 1779 "cost": cost, # current cost of all volume of instrument in basic asset 1780 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1781 "costRUB": costRUB, # cost of instrument in ruble 1782 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1783 "profit": profit, # expected profit at current moment 1784 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1785 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1786 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1787 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1788 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1789 "step": instrument["step"], # minimum price increment 1790 } 1791 1792 # adding distribution by unique countries: 1793 if statData["country"] not in byCountry.keys(): 1794 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1795 1796 else: 1797 byCountry[statData["country"]]["cost"] += costRUB 1798 byCountry[statData["country"]]["percent"] += percentCostRUB 1799 1800 if item["instrumentType"] != "currency": 1801 # adding distribution by unique companies: 1802 if statData["name"]: 1803 if statData["name"] not in byComp.keys(): 1804 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1805 1806 else: 1807 byComp[statData["name"]]["cost"] += costRUB 1808 byComp[statData["name"]]["percent"] += percentCostRUB 1809 1810 # adding distribution by unique sectors: 1811 if statData["sector"] not in bySect.keys(): 1812 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1813 1814 else: 1815 bySect[statData["sector"]]["cost"] += costRUB 1816 bySect[statData["sector"]]["percent"] += percentCostRUB 1817 1818 # adding distribution by unique currencies: 1819 if currency not in byCurr.keys(): 1820 byCurr[currency] = { 1821 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1822 "cost": costRUB, 1823 "percent": percentCostRUB 1824 } 1825 1826 else: 1827 byCurr[currency]["cost"] += costRUB 1828 byCurr[currency]["percent"] += percentCostRUB 1829 1830 # saving statistics for every instrument: 1831 if item["instrumentType"] == "currency": 1832 view["stat"]["Currencies"].append(statData) 1833 1834 # update dict with free funds for trading (total - blocked) by currencies 1835 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1836 view["stat"]["funds"][currency] = { 1837 "total": volume, 1838 "totalCostRUB": costRUB, # total volume cost in rubles 1839 "free": volume - blocked, 1840 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1841 } 1842 1843 elif item["instrumentType"] == "share": 1844 view["stat"]["Shares"].append(statData) 1845 1846 elif item["instrumentType"] == "bond": 1847 view["stat"]["Bonds"].append(statData) 1848 1849 elif item["instrumentType"] == "etf": 1850 view["stat"]["Etfs"].append(statData) 1851 1852 elif item["instrumentType"] == "Futures": 1853 view["stat"]["Futures"].append(statData) 1854 1855 else: 1856 continue 1857 1858 # total changes in Russian Ruble: 1859 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1860 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1861 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1862 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1863 view["stat"]["funds"]["rub"] = { 1864 "total": view["stat"]["availableRUB"], 1865 "totalCostRUB": view["stat"]["availableRUB"], 1866 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1867 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1868 } 1869 1870 # --- pending orders sector data: 1871 uniquePendingOrders = [] 1872 uniquePendingOrdersFIGIs = [] 1873 for item in view["raw"]["orders"]: 1874 if item["figi"] not in uniquePendingOrdersFIGIs: 1875 uniquePendingOrdersFIGIs.append(item["figi"]) 1876 uniquePendingOrders.append(item) 1877 1878 for item in uniquePendingOrders: 1879 self.figi = item["figi"] 1880 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1881 1882 if instrument: 1883 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1884 orderType = TKS_ORDER_TYPES[item["orderType"]] 1885 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1886 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1887 1888 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1889 if item["direction"] == "ORDER_DIRECTION_BUY": 1890 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1891 1892 else: 1893 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1894 1895 # requested price for order execution: 1896 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1897 1898 # necessary changes in percent to reach target from current price: 1899 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1900 1901 view["stat"]["orders"].append({ 1902 "orderID": item["orderId"], # orderId number parameter of current order 1903 "figi": item["figi"], # FIGI identification 1904 "ticker": instrument["ticker"], # ticker name by FIGI 1905 "lotsRequested": item["lotsRequested"], # requested lots value 1906 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1907 "currentPrice": lastPrice, # current instrument's price for defined action 1908 "targetPrice": target, # requested price for order execution in base currency 1909 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1910 "percentChanges": changes, # changes in percent to target from current price 1911 "currency": item["currency"], # instrument's currency name 1912 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1913 "type": orderType, # type of order from TKS_ORDER_TYPES 1914 "status": orderState, # order status from TKS_ORDER_STATES 1915 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1916 }) 1917 1918 # --- stop orders sector data: 1919 uniqueStopOrders = [] 1920 uniqueStopOrdersFIGIs = [] 1921 for item in view["raw"]["stopOrders"]: 1922 if item["figi"] not in uniqueStopOrdersFIGIs: 1923 uniqueStopOrdersFIGIs.append(item["figi"]) 1924 uniqueStopOrders.append(item) 1925 1926 for item in uniqueStopOrders: 1927 self.figi = item["figi"] 1928 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1929 1930 if instrument: 1931 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1932 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1933 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1934 1935 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1936 if "expirationTime" in item.keys(): 1937 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1938 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1939 1940 else: 1941 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1942 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1943 1944 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1945 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1946 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1947 1948 else: 1949 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1950 1951 # requested price when stop-order executed: 1952 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1953 1954 # price for limit-order, set up when stop-order executed: 1955 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1956 1957 # necessary changes in percent to reach target from current price: 1958 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1959 1960 view["stat"]["stopOrders"].append({ 1961 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1962 "figi": item["figi"], # FIGI identification 1963 "ticker": instrument["ticker"], # ticker name by FIGI 1964 "lotsRequested": item["lotsRequested"], # requested lots value 1965 "currentPrice": lastPrice, # current instrument's price for defined action 1966 "targetPrice": target, # requested price for stop-order execution in base currency 1967 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1968 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1969 "percentChanges": changes, # changes in percent to target from current price 1970 "currency": item["currency"], # instrument's currency name 1971 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1972 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1973 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1974 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1975 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1976 }) 1977 1978 # --- calculating data for analytics section: 1979 # portfolio distribution by assets: 1980 view["analytics"]["distrByAssets"] = { 1981 "Ruble": { 1982 "uniques": 1, 1983 "cost": view["stat"]["availableRUB"], 1984 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1985 }, 1986 "Currencies": { 1987 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 1988 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 1989 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1990 }, 1991 "Shares": { 1992 "uniques": len(view["stat"]["Shares"]), 1993 "cost": view["stat"]["sharesCostRUB"], 1994 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 1995 }, 1996 "Bonds": { 1997 "uniques": len(view["stat"]["Bonds"]), 1998 "cost": view["stat"]["bondsCostRUB"], 1999 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2000 }, 2001 "Etfs": { 2002 "uniques": len(view["stat"]["Etfs"]), 2003 "cost": view["stat"]["etfsCostRUB"], 2004 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2005 }, 2006 "Futures": { 2007 "uniques": len(view["stat"]["Futures"]), 2008 "cost": view["stat"]["futuresCostRUB"], 2009 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2010 }, 2011 } 2012 2013 # portfolio distribution by companies: 2014 view["analytics"]["distrByCompanies"]["All money cash"] = { 2015 "ticker": "", 2016 "cost": view["stat"]["allCurrenciesCostRUB"], 2017 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 } 2019 view["analytics"]["distrByCompanies"].update(byComp) 2020 2021 # portfolio distribution by sectors: 2022 view["analytics"]["distrBySectors"]["All money cash"] = { 2023 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2024 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2025 } 2026 view["analytics"]["distrBySectors"].update(bySect) 2027 2028 # portfolio distribution by currencies: 2029 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2030 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2031 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2032 2033 view["analytics"]["distrByCurrencies"].update(byCurr) 2034 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2035 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2036 2037 # portfolio distribution by countries: 2038 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2039 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2040 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2041 2042 view["analytics"]["distrByCountries"].update(byCountry) 2043 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2044 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2045 2046 # --- Prepare text statistics overview in human-readable: 2047 if show: 2048 # Whatever the value `details`, header not changes: 2049 info = [ 2050 "# Client's portfolio\n\n", 2051 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2052 "* **Account ID:** [{}]\n".format(self.accountId), 2053 ] 2054 2055 if details in ["full", "positions", "digest"]: 2056 info.extend([ 2057 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2058 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2059 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2060 view["stat"]["totalChangesRUB"], 2061 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2062 view["stat"]["totalChangesPercentRUB"], 2063 ), 2064 ]) 2065 2066 if details in ["full", "positions"]: 2067 info.extend([ 2068 "## Open positions\n\n", 2069 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2070 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2071 "| Ruble | {:>31} | | | | | |\n".format( 2072 "{:.2f} ({:.2f}) rub".format( 2073 view["stat"]["availableRUB"], 2074 view["stat"]["blockedRUB"], 2075 ) 2076 ) 2077 ]) 2078 2079 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2080 return [ 2081 "| | | | | | | |\n", 2082 "| {:<27} | | | | | {:>19} | |\n".format( 2083 noTradeStr if noTradeStr else typeStr, 2084 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2085 ), 2086 ] 2087 2088 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2089 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2090 "{} [{}]".format(data["ticker"], data["figi"]), 2091 "{:.2f} ({:.2f}) {}".format( 2092 data["volume"], 2093 data["blocked"], 2094 data["currency"], 2095 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2096 data["volume"], 2097 data["blocked"], 2098 ), 2099 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2100 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2101 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2102 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2103 "{}{:.2f} {} ({}{:.2f}%)".format( 2104 "+" if data["profit"] > 0 else "", 2105 data["profit"], data["baseCurrencyName"], 2106 "+" if data["percentProfit"] > 0 else "", 2107 data["percentProfit"], 2108 ), 2109 ) 2110 2111 # --- Show currencies section: 2112 if view["stat"]["Currencies"]: 2113 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2114 for item in view["stat"]["Currencies"]: 2115 info.append(_InfoStr(item, showCurrencyName=True)) 2116 2117 else: 2118 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2119 2120 # --- Show shares section: 2121 if view["stat"]["Shares"]: 2122 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2123 2124 for item in view["stat"]["Shares"]: 2125 info.append(_InfoStr(item)) 2126 2127 else: 2128 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2129 2130 # --- Show bonds section: 2131 if view["stat"]["Bonds"]: 2132 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2133 2134 for item in view["stat"]["Bonds"]: 2135 info.append(_InfoStr(item)) 2136 2137 else: 2138 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2139 2140 # --- Show etfs section: 2141 if view["stat"]["Etfs"]: 2142 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2143 2144 for item in view["stat"]["Etfs"]: 2145 info.append(_InfoStr(item)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2149 2150 # --- Show futures section: 2151 if view["stat"]["Futures"]: 2152 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2153 2154 for item in view["stat"]["Futures"]: 2155 info.append(_InfoStr(item)) 2156 2157 else: 2158 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2159 2160 if details in ["full", "orders"]: 2161 # --- Show pending orders section: 2162 if view["stat"]["orders"]: 2163 info.extend([ 2164 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2165 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2166 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2167 ]) 2168 2169 for item in view["stat"]["orders"]: 2170 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2171 "{} [{}]".format(item["ticker"], item["figi"]), 2172 item["orderID"], 2173 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2174 "{} {} ({}{:.2f}%)".format( 2175 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2176 item["baseCurrencyName"], 2177 "+" if item["percentChanges"] > 0 else "", 2178 float(item["percentChanges"]), 2179 ), 2180 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2181 item["action"], 2182 item["type"], 2183 item["date"], 2184 )) 2185 2186 else: 2187 info.append("\n## Total pending limit-orders: 0\n") 2188 2189 # --- Show stop orders section: 2190 if view["stat"]["stopOrders"]: 2191 info.extend([ 2192 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2193 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2194 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2195 ]) 2196 2197 for item in view["stat"]["stopOrders"]: 2198 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2199 "{} [{}]".format(item["ticker"], item["figi"]), 2200 item["orderID"], 2201 item["lotsRequested"], 2202 "{} {} ({}{:.2f}%)".format( 2203 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2204 item["baseCurrencyName"], 2205 "+" if item["percentChanges"] > 0 else "", 2206 float(item["percentChanges"]), 2207 ), 2208 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2209 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2210 item["action"], 2211 item["type"], 2212 item["expType"], 2213 item["createDate"], 2214 item["expDate"], 2215 )) 2216 2217 else: 2218 info.append("\n## Total stop-orders: 0\n") 2219 2220 if details in ["full", "analytics"]: 2221 # -- Show analytics section: 2222 if view["stat"]["portfolioCostRUB"] > 0: 2223 info.extend([ 2224 "\n# Analytics\n" 2225 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2226 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2227 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2228 view["stat"]["totalChangesRUB"], 2229 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2230 view["stat"]["totalChangesPercentRUB"], 2231 ), 2232 "\n## Portfolio distribution by assets\n" 2233 "\n| Type | Uniques | Percent | Current cost |\n", 2234 "|------------|---------|---------|--------------------|\n", 2235 ]) 2236 2237 for key in view["analytics"]["distrByAssets"].keys(): 2238 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2239 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2240 key, 2241 view["analytics"]["distrByAssets"][key]["uniques"], 2242 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2243 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2244 )) 2245 2246 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2247 info.extend([ 2248 "\n## Portfolio distribution by companies\n" 2249 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2250 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2251 ]) 2252 2253 for company in view["analytics"]["distrByCompanies"].keys(): 2254 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2255 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2256 info.append("| {} | {:<7} | {:<18} |\n".format( 2257 "{}{}{}".format( 2258 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2259 company, 2260 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2261 ), 2262 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2263 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2264 )) 2265 2266 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2267 info.extend([ 2268 "\n## Portfolio distribution by sectors\n" 2269 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2270 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2271 ]) 2272 2273 for sector in view["analytics"]["distrBySectors"].keys(): 2274 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2275 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2276 sector, 2277 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2278 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2279 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2280 )) 2281 2282 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2283 info.extend([ 2284 "\n## Portfolio distribution by currencies\n" 2285 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2286 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2287 ]) 2288 2289 for curr in view["analytics"]["distrByCurrencies"].keys(): 2290 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2291 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2292 info.append("| {} | {:<7} | {:<18} |\n".format( 2293 "[{}] {}{}".format( 2294 curr, 2295 view["analytics"]["distrByCurrencies"][curr]["name"], 2296 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2297 ), 2298 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2299 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2300 )) 2301 2302 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2303 info.extend([ 2304 "\n## Portfolio distribution by countries\n" 2305 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2306 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2307 ]) 2308 2309 for country in view["analytics"]["distrByCountries"].keys(): 2310 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2311 nameLen = len(country) 2312 info.append("| {} | {:<7} | {:<18} |\n".format( 2313 "{}{}".format( 2314 country, 2315 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2316 ), 2317 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2318 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2319 )) 2320 2321 infoText = "".join(info) 2322 2323 uLogger.info(infoText) 2324 2325 if details == "full" and self.overviewFile: 2326 filename = self.overviewFile 2327 2328 elif details == "digest" and self.overviewDigestFile: 2329 filename = self.overviewDigestFile 2330 2331 elif details == "positions" and self.overviewPositionsFile: 2332 filename = self.overviewPositionsFile 2333 2334 elif details == "orders" and self.overviewOrdersFile: 2335 filename = self.overviewOrdersFile 2336 2337 elif details == "analytics" and self.overviewAnalyticsFile: 2338 filename = self.overviewAnalyticsFile 2339 2340 else: 2341 filename = "" 2342 2343 if filename: 2344 with open(filename, "w", encoding="UTF-8") as fH: 2345 fH.write(infoText) 2346 2347 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2348 2349 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be? You should specify one of strings:
full- shows full available information about portfolio status (by default),positions- shows only open positions,digest- show a short digest of the portfolio status,analytics- shows only the analytics section and the distribution of the portfolio by various categories,orders- shows only sections of open limits and stop orders.
Returns
dictionary with client's raw portfolio and some statistics.
2351 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2352 """ 2353 Returns history operations between two given dates for current `accountId`. 2354 If `reportFile` string is not empty then also save human-readable report. 2355 Shows some statistical data of closed positions. 2356 2357 :param start: see docstring in `GetDatesAsString()` method 2358 :param end: see docstring in `GetDatesAsString()` method 2359 :param show: if `True` then also prints all records to the console. 2360 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2361 :return: original list of dictionaries with history of deals records from API ("operations" key): 2362 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2363 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2364 """ 2365 if self.accountId is None or not self.accountId: 2366 uLogger.error("Variable `accountId` must be defined for using this method!") 2367 raise Exception("Account ID required") 2368 2369 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2370 2371 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2372 2373 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2374 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2375 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2376 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2377 customStat = {} # custom statistics in additional to responseJSON 2378 2379 # --- output report in human-readable format: 2380 if show or self.reportFile: 2381 splitLine1 = "| | | | | |\n" # Summary section 2382 splitLine2 = "| | | | | | | | |\n" # Operations section 2383 nextDay = "" 2384 2385 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2386 2387 if len(ops) > 0: 2388 customStat = { 2389 "opsCount": 0, # total operations count 2390 "buyCount": 0, # buy operations 2391 "sellCount": 0, # sell operations 2392 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2393 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2394 "payIn": {"rub": 0.}, # Deposit brokerage account 2395 "payOut": {"rub": 0.}, # Withdrawals 2396 "divs": {"rub": 0.}, # Dividends income 2397 "coupons": {"rub": 0.}, # Coupon's income 2398 "brokerCom": {"rub": 0.}, # Service commissions 2399 "serviceCom": {"rub": 0.}, # Service commissions 2400 "marginCom": {"rub": 0.}, # Margin commissions 2401 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2402 } 2403 2404 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2405 for item in ops: 2406 if item["state"] == "OPERATION_STATE_EXECUTED": 2407 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2408 2409 # count buy operations: 2410 if "_BUY" in item["operationType"]: 2411 customStat["buyCount"] += 1 2412 2413 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2414 customStat["buyTotal"][item["payment"]["currency"]] += payment 2415 2416 else: 2417 customStat["buyTotal"][item["payment"]["currency"]] = payment 2418 2419 # count sell operations: 2420 elif "_SELL" in item["operationType"]: 2421 customStat["sellCount"] += 1 2422 2423 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2424 customStat["sellTotal"][item["payment"]["currency"]] += payment 2425 2426 else: 2427 customStat["sellTotal"][item["payment"]["currency"]] = payment 2428 2429 # count incoming operations: 2430 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2431 if item["payment"]["currency"] in customStat["payIn"].keys(): 2432 customStat["payIn"][item["payment"]["currency"]] += payment 2433 2434 else: 2435 customStat["payIn"][item["payment"]["currency"]] = payment 2436 2437 # count withdrawals operations: 2438 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2439 if item["payment"]["currency"] in customStat["payOut"].keys(): 2440 customStat["payOut"][item["payment"]["currency"]] += payment 2441 2442 else: 2443 customStat["payOut"][item["payment"]["currency"]] = payment 2444 2445 # count dividends income: 2446 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2447 if item["payment"]["currency"] in customStat["divs"].keys(): 2448 customStat["divs"][item["payment"]["currency"]] += payment 2449 2450 else: 2451 customStat["divs"][item["payment"]["currency"]] = payment 2452 2453 # count coupon's income: 2454 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2455 if item["payment"]["currency"] in customStat["coupons"].keys(): 2456 customStat["coupons"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["coupons"][item["payment"]["currency"]] = payment 2460 2461 # count broker commissions: 2462 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2463 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2464 customStat["brokerCom"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["brokerCom"][item["payment"]["currency"]] = payment 2468 2469 # count service commissions: 2470 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2471 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2472 customStat["serviceCom"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["serviceCom"][item["payment"]["currency"]] = payment 2476 2477 # count margin commissions: 2478 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2479 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2480 customStat["marginCom"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["marginCom"][item["payment"]["currency"]] = payment 2484 2485 # count withholding taxes: 2486 elif "_TAX" in item["operationType"]: 2487 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2488 customStat["allTaxes"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["allTaxes"][item["payment"]["currency"]] = payment 2492 2493 else: 2494 continue 2495 2496 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2497 2498 # --- view "Actions" lines: 2499 info.extend([ 2500 "| 1 | 2 | 3 | 4 | 5 |\n", 2501 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2502 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2503 "| | Buy: {:<22} | {:<28} | | |\n".format( 2504 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2505 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2506 ), 2507 "| | Sell: {:<21} | {:<28} | | |\n".format( 2508 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2509 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2510 ), 2511 ]) 2512 2513 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2514 for key in opsKeys: 2515 if key == "rub": 2516 continue 2517 2518 info.extend([ 2519 "| | | {:<28} | | |\n".format( 2520 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2521 ), 2522 "| | | {:<28} | | |\n".format( 2523 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2524 ), 2525 ]) 2526 2527 info.append(splitLine1) 2528 2529 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2530 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2531 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2532 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2533 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2534 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2535 ) 2536 2537 # --- view "Payments" lines: 2538 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2539 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2540 2541 for key in paymentsKeys: 2542 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2543 2544 info.append(splitLine1) 2545 2546 # --- view "Commissions and taxes" lines: 2547 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2548 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2549 2550 for key in comKeys: 2551 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2552 2553 info.append(splitLine1) 2554 2555 info.extend([ 2556 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2557 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2558 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2559 ]) 2560 2561 else: 2562 info.append("Broker returned no operations during this period\n") 2563 2564 # --- view "Operations" section: 2565 for item in ops: 2566 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2567 continue 2568 2569 else: 2570 self.figi = item["figi"] if item["figi"] else "" 2571 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2572 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2573 2574 # group of deals during one day: 2575 if nextDay and item["date"].split("T")[0] != nextDay: 2576 info.append(splitLine2) 2577 nextDay = "" 2578 2579 else: 2580 nextDay = item["date"].split("T")[0] # saving current day for splitting 2581 2582 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2583 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2584 self.figi if self.figi else "—", 2585 instrument["ticker"] if instrument else "—", 2586 instrument["type"] if instrument else "—", 2587 item["quantity"] if int(item["quantity"]) > 0 else "—", 2588 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2589 TKS_OPERATION_STATES[item["state"]], 2590 TKS_OPERATION_TYPES[item["operationType"]], 2591 )) 2592 2593 infoText = "".join(info) 2594 2595 if show: 2596 uLogger.info(infoText) 2597 2598 if self.reportFile: 2599 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2600 fH.write(infoText) 2601 2602 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2603 2604 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2606 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2607 """ 2608 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2609 2610 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2611 Warning! Broker server used ISO UTC time by default. 2612 2613 If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe. 2614 Also, `historyFile` used to update history with `onlyMissing` parameter. 2615 2616 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2617 2618 :param start: see docstring in `GetDatesAsString()` method. 2619 :param end: see docstring in `GetDatesAsString()` method. 2620 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2621 `"hour"`, `"day"`. Default: `"hour"`. 2622 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2623 False by default. Warning! History appends only from last candle to current time 2624 with always update last candle! 2625 :param csvSep: separator if csv-file is used, `,` by default. 2626 :param show: if `True` then also prints pandas dataframe to the console. 2627 :return: pandas dataframe with prices history. Headers of columns are defined by default: 2628 `["date", "time", "open", "high", "low", "close", "volume"]`. 2629 """ 2630 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2631 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2632 history = None # empty pandas object for history 2633 2634 if interval not in TKS_CANDLE_INTERVALS.keys(): 2635 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2636 raise Exception("Incorrect value") 2637 2638 if not (self.ticker or self.figi): 2639 uLogger.error("Ticker or FIGI must be defined!") 2640 raise Exception("Ticker or FIGI required") 2641 2642 if self.ticker and not self.figi: 2643 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2644 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2645 2646 if self.figi and not self.ticker: 2647 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2648 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2649 2650 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2651 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2652 if interval.lower() != "day": 2653 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2654 2655 delta = dtEnd - dtStart # current UTC time minus last time in file 2656 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2657 2658 # calculate history length in candles: 2659 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2660 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2661 length += 1 # to avoid fraction time 2662 2663 # calculate data blocks count: 2664 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2665 2666 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2667 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2668 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2669 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2670 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2671 2672 tempOld = None # pandas object for old history, if --only-missing key present 2673 lastTime = None # datetime object of last old candle in file 2674 2675 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2676 uLogger.debug("--only-missing key present, add only last missing candles...") 2677 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2678 2679 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2680 2681 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2682 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2683 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2684 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2685 2686 # get last datetime object from last string in file or minus 1 delta if file is empty: 2687 if len(tempOld) > 0: 2688 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2689 2690 else: 2691 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2692 2693 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2694 2695 responseJSONs = [] # raw history blocks of data 2696 2697 blockEnd = dtEnd 2698 for item in range(blocks): 2699 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2700 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2701 2702 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2703 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2704 )) 2705 2706 if blockStart == blockEnd: 2707 uLogger.debug("Skipped this zero-length block...") 2708 2709 else: 2710 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2711 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2712 self.body = str({ 2713 "figi": self.figi, 2714 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2715 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2716 "interval": TKS_CANDLE_INTERVALS[interval][0] 2717 }) 2718 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2719 2720 if "code" in responseJSON.keys(): 2721 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2722 2723 else: 2724 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2725 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2726 2727 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2728 2729 blockEnd = blockStart 2730 2731 printCount = len(responseJSONs) # candles to show in console 2732 if responseJSONs: 2733 tempHistory = pd.DataFrame( 2734 data={ 2735 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2736 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2737 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2738 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2739 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2740 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2741 "volume": [int(item["volume"]) for item in responseJSONs], 2742 }, 2743 index=range(len(responseJSONs)), 2744 columns=["date", "time", "open", "high", "low", "close", "volume"], 2745 ) 2746 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2747 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2748 2749 # append only newest candles to old history if --only-missing key present: 2750 if onlyMissing and tempOld is not None and lastTime is not None: 2751 index = 0 # find start index in tempHistory data: 2752 2753 for i, item in tempHistory.iterrows(): 2754 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2755 2756 if curTime == lastTime: 2757 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2758 index = i 2759 printCount = index + 1 2760 break 2761 2762 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2763 2764 else: 2765 history = tempHistory # if no `--only-missing` key then load full data from server 2766 2767 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2768 2769 if history is not None and not history.empty: 2770 if show: 2771 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2772 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2773 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2774 )) 2775 2776 else: 2777 uLogger.warning("Received an empty candles history!") 2778 2779 if self.historyFile is not None: 2780 if history is not None and not history.empty: 2781 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2782 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2783 2784 else: 2785 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2786 2787 else: 2788 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.") 2789 2790 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only pandas dataframe.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints pandas dataframe to the console.
Returns
pandas dataframe with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2792 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2793 """ 2794 Load candles history from csv-file and return pandas dataframe object. 2795 2796 See also: `History()` and `ShowHistoryChart()` methods. 2797 2798 :param filePath: path to csv-file to open. 2799 """ 2800 loadedHistory = None # init candles data object 2801 2802 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2803 2804 if os.path.exists(filePath): 2805 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as pandas dataframe 2806 2807 tfStr = self.priceModel.FormattedDelta( 2808 self.priceModel.timeframe, 2809 "{days} days {hours}h {minutes}m {seconds}s", 2810 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2811 self.priceModel.timeframe, 2812 "{hours}h {minutes}m {seconds}s", 2813 ) 2814 2815 if loadedHistory is not None and not loadedHistory.empty: 2816 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2817 len(loadedHistory), 2818 tfStr, 2819 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2820 ) 2821 2822 else: 2823 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2824 2825 else: 2826 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2827 2828 return loadedHistory
Load candles history from csv-file and return pandas dataframe object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2830 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2831 """ 2832 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2833 2834 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2835 Default: `index.html` (both for interact and non-interact candlesticks chart). 2836 2837 See also: `History()` and `LoadHistory()` methods. 2838 2839 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2840 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2841 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2842 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2843 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2844 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2845 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2846 """ 2847 if isinstance(candles, str): 2848 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2849 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2850 2851 elif isinstance(candles, pd.DataFrame): 2852 self.priceModel.prices = candles # set candles chain from variable 2853 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2854 2855 if "datetime" not in candles.columns: 2856 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2857 2858 else: 2859 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2860 raise Exception("Incorrect value") 2861 2862 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2863 2864 if interact: 2865 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2866 2867 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2868 2869 else: 2870 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2871 2872 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2873 2874 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2876 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2877 """ 2878 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2879 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2880 2881 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2882 2883 :param operation: string "Buy" or "Sell". 2884 :param lots: volume, integer count of lots >= 1. 2885 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2886 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2887 :param expDate: string "Undefined" by default or local date in future, 2888 it is a string with format `%Y-%m-%d %H:%M:%S`. 2889 :return: JSON with response from broker server. 2890 """ 2891 if self.accountId is None or not self.accountId: 2892 uLogger.error("Variable `accountId` must be defined for using this method!") 2893 raise Exception("Account ID required") 2894 2895 if operation is None or not operation or operation not in ("Buy", "Sell"): 2896 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2897 raise Exception("Incorrect value") 2898 2899 if lots is None or lots < 1: 2900 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2901 lots = 1 2902 2903 if tp is None or tp < 0: 2904 tp = 0 2905 2906 if sl is None or sl < 0: 2907 sl = 0 2908 2909 if expDate is None or not expDate: 2910 expDate = "Undefined" 2911 2912 if not (self.ticker or self.figi): 2913 uLogger.error("Ticker or FIGI must be defined!") 2914 raise Exception("Ticker or FIGI required") 2915 2916 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2917 self.ticker = instrument["ticker"] 2918 self.figi = instrument["figi"] 2919 2920 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2921 2922 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2923 self.body = str({ 2924 "figi": self.figi, 2925 "quantity": str(lots), 2926 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2927 "accountId": str(self.accountId), 2928 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2929 }) 2930 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2931 2932 if "orderId" in response.keys(): 2933 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2934 operation, response["orderId"], 2935 self.ticker, self.figi, lots, 2936 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2937 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2938 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2939 )) 2940 2941 else: 2942 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2943 2944 if tp > 0: 2945 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2946 2947 if sl > 0: 2948 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2949 2950 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2952 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2953 """ 2954 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2955 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2956 2957 See also: `Order()` and `Trade()` docstrings. 2958 2959 :param lots: volume, integer count of lots >= 1. 2960 :param tp: float > 0, take profit price of stop-order. 2961 :param sl: float > 0, stop loss price of stop-order. 2962 :param expDate: it's a local date in future. 2963 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2964 :return: JSON with response from broker server. 2965 """ 2966 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2968 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2969 """ 2970 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2971 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2972 2973 See also: `Order()` and `Trade()` docstrings. 2974 2975 :param lots: volume, integer count of lots >= 1. 2976 :param tp: float > 0, take profit price of stop-order. 2977 :param sl: float > 0, stop loss price of stop-order. 2978 :param expDate: it's a local date in the future. 2979 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2980 :return: JSON with response from broker server. 2981 """ 2982 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2984 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 2985 """ 2986 Close position of given instruments. 2987 2988 :param tickers: tickers list of instruments that must be closed. 2989 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 2990 This avoids unnecessary downloading data from the server. 2991 """ 2992 if not tickers: 2993 uLogger.info("Tickers list is empty, nothing to close.") 2994 2995 else: 2996 if portfolio is None or not portfolio: 2997 portfolio = self.Overview(show=False) 2998 2999 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3000 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3001 3002 for ticker in tickers: 3003 if ticker not in allOpenedTickers: 3004 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3005 continue 3006 3007 # search open trade info about instrument by ticker: 3008 instrument = {} 3009 for iType in TKS_INSTRUMENTS: 3010 if instrument: 3011 break 3012 3013 for item in portfolio["stat"][iType]: 3014 if item["ticker"] == ticker: 3015 instrument = item 3016 break 3017 3018 if instrument: 3019 self.ticker = ticker 3020 self.figi = instrument["figi"] 3021 3022 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3023 self.ticker, 3024 self.figi, 3025 int(instrument["volume"]), 3026 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3027 )) 3028 3029 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3030 3031 if tradeLots > 0: 3032 if instrument["blocked"] > 0: 3033 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3034 instrument["blocked"], 3035 self.ticker, 3036 tradeLots, 3037 )) 3038 3039 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3040 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3041 3042 else: 3043 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- tickers: tickers list of instruments that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3045 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3046 """ 3047 Close all positions of given instruments with defined type. 3048 3049 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3050 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3051 This avoids unnecessary downloading data from the server. 3052 """ 3053 if iType not in TKS_INSTRUMENTS: 3054 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3055 3056 else: 3057 if portfolio is None or not portfolio: 3058 portfolio = self.Overview(show=False) 3059 3060 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3061 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3062 3063 if tickers and portfolio: 3064 self.CloseTrades(tickers, portfolio) 3065 3066 else: 3067 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3069 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3070 """ 3071 Universal method to create market or limit orders with all available parameters for current `accountId`. 3072 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3073 3074 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3075 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3076 3077 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3078 then broker immediately open market order as you can do simple --buy or --sell operations! 3079 3080 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3081 When current price will go up or down to target price value then broker opens a limit order. 3082 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3083 3084 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3085 3086 :param operation: string "Buy" or "Sell". 3087 :param orderType: string "Limit" or "Stop". 3088 :param lots: volume, integer count of lots >= 1. 3089 :param targetPrice: target price > 0. This is open trade price for limit order. 3090 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3091 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3092 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3093 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3094 Stop loss order always executed by market price. 3095 :param expDate: string "Undefined" by default or local date in future. 3096 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3097 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3098 A limit order has no expiration date, it lasts until the end of the trading day. 3099 :return: JSON with response from broker server. 3100 """ 3101 if self.accountId is None or not self.accountId: 3102 uLogger.error("Variable `accountId` must be defined for using this method!") 3103 raise Exception("Account ID required") 3104 3105 if operation is None or not operation or operation not in ("Buy", "Sell"): 3106 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3107 raise Exception("Incorrect value") 3108 3109 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3110 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3111 raise Exception("Incorrect value") 3112 3113 if lots is None or lots < 1: 3114 uLogger.error("You must define trade volume > 0: integer count of lots!") 3115 raise Exception("Incorrect value") 3116 3117 if targetPrice is None or targetPrice <= 0: 3118 uLogger.error("Target price for limit-order must be greater than 0!") 3119 raise Exception("Incorrect value") 3120 3121 if limitPrice is None or limitPrice <= 0: 3122 limitPrice = targetPrice 3123 3124 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3125 stopType = "Limit" 3126 3127 if expDate is None or not expDate: 3128 expDate = "Undefined" 3129 3130 if not (self.ticker or self.figi): 3131 uLogger.error("Tocker or FIGI must be defined!") 3132 raise Exception("Ticker or FIGI required") 3133 3134 response = {} 3135 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3136 self.ticker = instrument["ticker"] 3137 self.figi = instrument["figi"] 3138 3139 if orderType == "Limit": 3140 uLogger.debug( 3141 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3142 self.ticker, self.figi, 3143 operation, lots, targetPrice, instrument["currency"], 3144 )) 3145 3146 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3147 self.body = str({ 3148 "figi": self.figi, 3149 "quantity": str(lots), 3150 "price": FloatToNano(targetPrice), 3151 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3152 "accountId": str(self.accountId), 3153 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3154 }) 3155 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3156 3157 if "orderId" in response.keys(): 3158 uLogger.info( 3159 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3160 response["orderId"], 3161 self.ticker, self.figi, 3162 operation, lots, targetPrice, instrument["currency"], 3163 )) 3164 3165 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3166 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3167 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3168 targetPrice, instrument["currency"], 3169 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3170 )) 3171 3172 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3173 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3174 targetPrice, instrument["currency"], 3175 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3176 )) 3177 3178 else: 3179 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3180 3181 if orderType == "Stop": 3182 uLogger.debug( 3183 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3184 self.ticker, self.figi, 3185 operation, lots, 3186 targetPrice, instrument["currency"], 3187 limitPrice, instrument["currency"], 3188 stopType, expDate, 3189 )) 3190 3191 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3192 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3193 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3194 3195 body = { 3196 "figi": self.figi, 3197 "quantity": str(lots), 3198 "price": FloatToNano(limitPrice), 3199 "stopPrice": FloatToNano(targetPrice), 3200 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3201 "accountId": str(self.accountId), 3202 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3203 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3204 } 3205 3206 if expDateUTC: 3207 body["expireDate"] = expDateUTC 3208 3209 self.body = str(body) 3210 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3211 3212 if "stopOrderId" in response.keys(): 3213 uLogger.info( 3214 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3215 response["stopOrderId"], 3216 self.ticker, self.figi, 3217 operation, lots, 3218 targetPrice, instrument["currency"], 3219 limitPrice, instrument["currency"], 3220 TKS_STOP_ORDER_TYPES[stopOrderType], 3221 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3222 )) 3223 3224 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3225 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3226 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3227 targetPrice, instrument["currency"], 3228 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3229 )) 3230 3231 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3232 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3233 targetPrice, instrument["currency"], 3234 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3235 )) 3236 3237 else: 3238 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3239 3240 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3242 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3243 """ 3244 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3245 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3246 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3247 See also: `Order()` docstring. 3248 3249 :param lots: volume, integer count of lots >= 1. 3250 :param targetPrice: target price > 0. This is open trade price for limit order. 3251 :return: JSON with response from broker server. 3252 """ 3253 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3255 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3256 """ 3257 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3258 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3259 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3260 target price value then broker opens a limit order. See also: `Order()` docstring. 3261 3262 :param lots: volume, integer count of lots >= 1. 3263 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3264 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3265 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3266 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3267 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3268 :param expDate: string "Undefined" by default or local date in future. 3269 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3270 This date is converting to UTC format for server. 3271 :return: JSON with response from broker server. 3272 """ 3273 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3275 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3276 """ 3277 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3278 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3279 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3280 See also: `Order()` docstring. 3281 3282 :param lots: volume, integer count of lots >= 1. 3283 :param targetPrice: target price > 0. This is open trade price for limit order. 3284 :return: JSON with response from broker server. 3285 """ 3286 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3288 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3289 """ 3290 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3291 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3292 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3293 target price value then broker opens a limit order. See also: `Order()` docstring. 3294 3295 :param lots: volume, integer count of lots >= 1. 3296 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3297 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3298 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3299 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3300 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3301 :param expDate: string "Undefined" by default or local date in future. 3302 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3303 This date is converting to UTC format for server. 3304 :return: JSON with response from broker server. 3305 """ 3306 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3308 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3309 """ 3310 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3311 3312 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3313 :param allOrdersIDs: pre-received lists of all active pending orders. 3314 This avoids unnecessary downloading data from the server. 3315 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3316 """ 3317 if self.accountId is None or not self.accountId: 3318 uLogger.error("Variable `accountId` must be defined for using this method!") 3319 raise Exception("Account ID required") 3320 3321 if orderIDs: 3322 if allOrdersIDs is None or not allOrdersIDs: 3323 rawOrders = self.RequestPendingOrders() 3324 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3325 3326 if allStopOrdersIDs is None or not allStopOrdersIDs: 3327 rawStopOrders = self.RequestStopOrders() 3328 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3329 3330 for orderID in orderIDs: 3331 idInPendingOrders = orderID in allOrdersIDs 3332 idInStopOrders = orderID in allStopOrdersIDs 3333 3334 if not (idInPendingOrders or idInStopOrders): 3335 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3336 continue 3337 3338 else: 3339 if idInPendingOrders: 3340 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3341 3342 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3343 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3344 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3345 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3346 3347 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3348 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3349 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3350 3351 else: 3352 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3353 3354 elif idInStopOrders: 3355 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3356 3357 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3358 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3359 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3360 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3361 3362 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3363 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3364 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3365 3366 else: 3367 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3368 3369 else: 3370 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3372 def CloseAllOrders(self) -> None: 3373 """ 3374 Gets a list of open pending and stop orders and cancel it all. 3375 """ 3376 rawOrders = self.RequestPendingOrders() 3377 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3378 lenOrders = len(allOrdersIDs) 3379 3380 rawStopOrders = self.RequestStopOrders() 3381 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3382 lenSOrders = len(allStopOrdersIDs) 3383 3384 if lenOrders > 0 or lenSOrders > 0: 3385 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3386 3387 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3388 3389 else: 3390 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3392 def CloseAll(self, *args) -> None: 3393 """ 3394 Close all available (not blocked) opened trades and orders. 3395 3396 Also, you can select one or more keywords case-insensitive: 3397 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3398 3399 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3400 """ 3401 overview = self.Overview(show=False) # get all open trades info 3402 3403 if len(args) == 0: 3404 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3405 self.CloseAllOrders() # close all pending and stop orders 3406 3407 for iType in TKS_INSTRUMENTS: 3408 if iType != "Currencies": 3409 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3410 3411 else: 3412 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3413 lowerArgs = [x.lower() for x in args] 3414 3415 if "orders" in lowerArgs: 3416 self.CloseAllOrders() # close all pending and stop orders 3417 3418 for iType in TKS_INSTRUMENTS: 3419 if iType.lower() in lowerArgs and iType != "Currencies": 3420 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3422 @staticmethod 3423 def ParseOrderParameters(operation, **inputParameters): 3424 """ 3425 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3426 3427 :param operation: string "Buy" or "Sell". 3428 :param inputParameters: this is dict of strings that looks like this 3429 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3430 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3431 "prices" key: one or more prices to open limit-orders 3432 Counts of values in lots and prices lists must be equals! 3433 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3434 """ 3435 # TODO: update order grid work with api v2 3436 pass 3437 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3438 # 3439 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3440 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3441 # raise Exception("Incorrect value") 3442 # 3443 # if "l" in inputParameters.keys(): 3444 # inputParameters["lots"] = inputParameters.pop("l") 3445 # 3446 # if "p" in inputParameters.keys(): 3447 # inputParameters["prices"] = inputParameters.pop("p") 3448 # 3449 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3450 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3451 # raise Exception("Incorrect value") 3452 # 3453 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3454 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3455 # 3456 # if len(lots) != len(prices): 3457 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3458 # raise Exception("Incorrect value") 3459 # 3460 # uLogger.debug("Extracted parameters for orders:") 3461 # uLogger.debug("lots = {}".format(lots)) 3462 # uLogger.debug("prices = {}".format(prices)) 3463 # 3464 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3465 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3466 # uLogger.debug("Order parameters: {}".format(result)) 3467 # 3468 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3470 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3471 """ 3472 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3473 3474 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3475 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3476 """ 3477 result = False 3478 msg = "Instrument not defined!" 3479 3480 if portfolio is None or not portfolio: 3481 portfolio = self.Overview(show=False) 3482 3483 if self.ticker: 3484 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3485 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3486 3487 for iType in TKS_INSTRUMENTS: 3488 for instrument in portfolio["stat"][iType]: 3489 if instrument["ticker"] == self.ticker: 3490 result = True 3491 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3492 break 3493 3494 elif self.figi: 3495 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3496 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3497 3498 for iType in TKS_INSTRUMENTS: 3499 for instrument in portfolio["stat"][iType]: 3500 if instrument["figi"] == self.figi: 3501 result = True 3502 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3503 break 3504 3505 else: 3506 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3507 3508 uLogger.debug(msg) 3509 3510 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3512 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3513 """ 3514 Returns instrument is in the user's portfolio if it presents there. 3515 Instrument must be defined by `ticker` (highly priority) or `figi`. 3516 3517 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3518 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3519 """ 3520 result = None 3521 msg = "Instrument not defined!" 3522 3523 if portfolio is None or not portfolio: 3524 portfolio = self.Overview(show=False) 3525 3526 if self.ticker: 3527 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3528 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3529 3530 for iType in TKS_INSTRUMENTS: 3531 for instrument in portfolio["stat"][iType]: 3532 if instrument["ticker"] == self.ticker: 3533 result = instrument 3534 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3535 break 3536 3537 elif self.figi: 3538 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3539 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3540 3541 for iType in TKS_INSTRUMENTS: 3542 for instrument in portfolio["stat"][iType]: 3543 if instrument["figi"] == self.figi: 3544 result = instrument 3545 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3546 break 3547 3548 else: 3549 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3550 3551 uLogger.debug(msg) 3552 3553 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3555 def RequestLimits(self) -> dict: 3556 """ 3557 Method for obtaining the available funds for withdrawal for current `accountId`. 3558 3559 See also: 3560 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3561 - `OverviewLimits()` method 3562 3563 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3564 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3565 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3566 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3567 """ 3568 if self.accountId is None or not self.accountId: 3569 uLogger.error("Variable `accountId` must be defined for using this method!") 3570 raise Exception("Account ID required") 3571 3572 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3573 3574 self.body = str({"accountId": self.accountId}) 3575 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3576 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3577 3578 uLogger.debug("Records about available funds for withdrawal successfully received") 3579 3580 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3582 def OverviewLimits(self, show: bool = False) -> dict: 3583 """ 3584 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3585 3586 See also: `RequestLimits()`. 3587 3588 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3589 :return: dict with raw parsed data from server and some calculated statistics about it. 3590 """ 3591 if self.accountId is None or not self.accountId: 3592 uLogger.error("Variable `accountId` must be defined for using this method!") 3593 raise Exception("Account ID required") 3594 3595 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3596 3597 view = { 3598 "rawLimits": rawLimits, 3599 "limits": { # parsed data for every currency: 3600 "money": { # this is an array of portfolio currency positions 3601 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3602 }, 3603 "blocked": { # this is an array of blocked currency 3604 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3605 }, 3606 "blockedGuarantee": { # this is locked money under collateral for futures 3607 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3608 }, 3609 }, 3610 } 3611 3612 # --- Prepare text table with limits in human-readable format: 3613 if show: 3614 info = [ 3615 "# Withdrawal limits\n\n", 3616 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3617 "* **Account ID:** [{}]\n".format(self.accountId), 3618 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3619 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3620 ] 3621 3622 for curr in view["limits"]["money"].keys(): 3623 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3624 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3625 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3626 3627 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3628 "[{}]".format(curr), 3629 "{:.2f}".format(view["limits"]["money"][curr]), 3630 "{:.2f}".format(availableMoney), 3631 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3632 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3633 ) 3634 3635 if curr == "rub": 3636 info.insert(5, infoStr) # insert at first position in table and after headers 3637 3638 else: 3639 info.append(infoStr) 3640 3641 infoText = "".join(info) 3642 3643 uLogger.info(infoText) 3644 3645 if self.withdrawalLimitsFile: 3646 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3647 fH.write(infoText) 3648 3649 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3650 3651 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3653 def RequestAccounts(self) -> dict: 3654 """ 3655 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3656 3657 See also: 3658 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3659 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3660 - `OverviewUserInfo()` method 3661 3662 :return: dict with raw data from server that contains accounts info. Example of dict: 3663 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3664 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3665 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3666 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3667 """ 3668 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3669 3670 self.body = str({}) 3671 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3672 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3673 3674 uLogger.debug("Records about available accounts successfully received") 3675 3676 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3678 def RequestUserInfo(self) -> dict: 3679 """ 3680 Method for requesting common user's information. 3681 3682 See also: 3683 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3684 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3685 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3686 - `OverviewUserInfo()` method 3687 3688 :return: dict with raw data from server that contains user's information. Example of dict: 3689 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3690 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3691 """ 3692 uLogger.debug("Requesting common user's information. Wait, please...") 3693 3694 self.body = str({}) 3695 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3696 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3697 3698 uLogger.debug("Records about current user successfully received") 3699 3700 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3702 def RequestMarginStatus(self, accountId: str = None) -> dict: 3703 """ 3704 Method for requesting margin calculation for defined account ID. 3705 3706 See also: 3707 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3708 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3709 - `OverviewUserInfo()` method 3710 3711 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3712 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3713 Example of responses: 3714 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3715 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3716 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3717 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3718 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3719 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3720 """ 3721 if accountId is None or not accountId: 3722 if self.accountId is None or not self.accountId: 3723 uLogger.error("Variable `accountId` must be defined for using this method!") 3724 raise Exception("Account ID required") 3725 3726 else: 3727 accountId = self.accountId # use `self.accountId` (main ID) by default 3728 3729 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3730 3731 self.body = str({"accountId": accountId}) 3732 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3733 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3734 3735 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3736 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3737 rawMargin = {} 3738 3739 else: 3740 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3741 3742 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3744 def RequestTariffLimits(self) -> dict: 3745 """ 3746 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3747 3748 See also: 3749 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3750 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3751 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3752 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3753 - `OverviewUserInfo()` method 3754 3755 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3756 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3757 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3758 """ 3759 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3760 3761 self.body = str({}) 3762 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3763 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3764 3765 uLogger.debug("Records with limits of current tariff successfully received") 3766 3767 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3769 def RequestBondCoupons(self, iJSON: dict) -> dict: 3770 """ 3771 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3772 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3773 All dates are in UTC timezone. 3774 3775 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3776 Documentation: 3777 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3778 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3779 3780 See also: `ExtendBondsData()`. 3781 3782 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3783 If raw iJSON is not data of bond then server returns an error [400] with message: 3784 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3785 :return: dictionary with bond payment calendar. Response example 3786 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3787 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3788 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3789 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3790 """ 3791 if iJSON["figi"] is None or not iJSON["figi"]: 3792 uLogger.error("FIGI must be defined for using this method!") 3793 raise Exception("FIGI required") 3794 3795 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3796 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3797 3798 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3799 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3800 self.figi, 3801 startDate, 3802 endDate, 3803 )) 3804 3805 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3806 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3807 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3808 3809 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3810 uLogger.warning("Instrument type is not bond!") 3811 3812 else: 3813 uLogger.debug("Records about bond payment calendar successfully received") 3814 3815 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3817 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3818 """ 3819 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3820 pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, 3821 coupon yields, current yields and some statistics etc. 3822 3823 WARNING! This is too long operation if a lot of bonds requested from broker server. 3824 3825 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3826 3827 :param instruments: list of strings with tickers or FIGIs. 3828 :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3829 for further used by data scientists or stock analytics. 3830 :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. 3831 In XLSX-file and pandas dataframe fields mean: 3832 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3833 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3834 """ 3835 if instruments is None or not instruments: 3836 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3837 raise Exception("Ticker or FIGI required") 3838 3839 if isinstance(instruments, str): 3840 instruments = [instruments] 3841 3842 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3843 3844 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3845 3846 iCount = len(uniqueInstruments) 3847 tooLong = iCount >= 20 3848 if tooLong: 3849 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3850 3851 bonds = None 3852 for i, self.figi in enumerate(uniqueInstruments): 3853 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3854 3855 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3856 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3857 rawBond = self.SearchByFIGI(requestPrice=True) 3858 3859 # Widen raw data with UTC current time (iData["actualDateTime"]): 3860 actualDate = datetime.now(tzutc()) 3861 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3862 3863 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3864 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3865 3866 # Replace some values with human-readable: 3867 iData["nominalCurrency"] = iData["nominal"]["currency"] 3868 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3869 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3870 iData["aciCurrency"] = iData["aciValue"]["currency"] 3871 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3872 iData["issueSize"] = int(iData["issueSize"]) 3873 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3874 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3875 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3876 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3877 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3878 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3879 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3880 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3881 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3882 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3883 3884 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3885 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3886 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3887 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3888 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3889 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3890 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3891 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3892 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3893 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3894 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3895 3896 # Widen raw data with calendar data from `rawCalendar` values: 3897 calendarData = [] 3898 for item in iData["rawCalendar"]["events"]: 3899 calendarData.append({ 3900 "couponDate": item["couponDate"], 3901 "couponNumber": int(item["couponNumber"]), 3902 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3903 "payCurrency": item["payOneBond"]["currency"], 3904 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3905 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3906 "couponStartDate": item["couponStartDate"], 3907 "couponEndDate": item["couponEndDate"], 3908 "couponPeriod": item["couponPeriod"], 3909 }) 3910 3911 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3912 if "maturityDate" not in iData.keys(): 3913 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3914 3915 # Widen raw data with Coupon Rate. 3916 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3917 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3918 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3919 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3920 3921 # Widen raw data with Yield to Maturity (YTM) on current date. 3922 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3923 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3924 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3925 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3926 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3927 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3928 3929 iData["calendar"] = calendarData # adds calendar at the end 3930 3931 # Remove not used data: 3932 iData.pop("uid") 3933 iData.pop("positionUid") 3934 iData.pop("currentPrice") 3935 iData.pop("rawCalendar") 3936 3937 colNames = list(iData.keys()) 3938 if bonds is None: 3939 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3940 3941 else: 3942 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3943 3944 else: 3945 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3946 3947 processed = round(100 * (i + 1) / iCount, 1) 3948 if tooLong and processed % 5 == 0: 3949 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3950 3951 else: 3952 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3953 3954 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3955 3956 # Saving bonds from pandas dataframe to XLSX sheet: 3957 if xlsx and self.bondsXLSXFile: 3958 with pd.ExcelWriter( 3959 path=self.bondsXLSXFile, 3960 date_format=TKS_DATE_FORMAT, 3961 datetime_format=TKS_DATE_TIME_FORMAT, 3962 mode="w", 3963 ) as writer: 3964 bonds.to_excel( 3965 writer, 3966 sheet_name="Extended bonds data", 3967 index=True, 3968 encoding="UTF-8", 3969 freeze_panes=(1, 1), 3970 ) # saving as XLSX-file with freeze first row and column as headers 3971 3972 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3973 3974 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports pandas dataframe to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. In XLSX-file and pandas dataframe fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3976 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 3977 """ 3978 Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, `calendar.xlsx` by default. 3979 3980 WARNING! This is too long operation if a lot of bonds requested from broker server. 3981 3982 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 3983 3984 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 3985 extended information about bonds: main info, current prices, bond payment calendar, 3986 coupon yields, current yields and some statistics etc. 3987 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 3988 :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 3989 for further used by data scientists or stock analytics. 3990 :return: pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 3991 """ 3992 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 3993 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 3994 3995 uLogger.debug("Generating bond payments calendar data. Wait, please...") 3996 3997 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 3998 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 3999 calendar = None 4000 for bond in extBonds.iterrows(): 4001 for item in bond[1]["calendar"]: 4002 cData = { 4003 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4004 "couponDate": item["couponDate"], 4005 "figi": bond[1]["figi"], 4006 "ticker": bond[1]["ticker"], 4007 "name": bond[1]["name"], 4008 "couponNumber": item["couponNumber"], 4009 "payOneBond": item["payOneBond"], 4010 "payCurrency": item["payCurrency"], 4011 "couponType": item["couponType"], 4012 "couponPeriod": item["couponPeriod"], 4013 "fixDate": item["fixDate"], 4014 "couponStartDate": item["couponStartDate"], 4015 "couponEndDate": item["couponEndDate"], 4016 } 4017 4018 if calendar is None: 4019 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4020 4021 else: 4022 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4023 4024 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4025 4026 # Saving calendar from pandas dataframe to XLSX sheet: 4027 if xlsx: 4028 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4029 4030 with pd.ExcelWriter( 4031 path=xlsxCalendarFile, 4032 date_format=TKS_DATE_FORMAT, 4033 datetime_format=TKS_DATE_TIME_FORMAT, 4034 mode="w", 4035 ) as writer: 4036 humanReadable = calendar.copy(deep=True) 4037 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4038 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4039 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4040 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4041 humanReadable.columns = colNames # human-readable column names 4042 4043 humanReadable.to_excel( 4044 writer, 4045 sheet_name="Bond payments calendar", 4046 index=False, 4047 encoding="UTF-8", 4048 freeze_panes=(1, 2), 4049 ) # saving as XLSX-file with freeze first row and column as headers 4050 4051 del humanReadable # release df in memory 4052 4053 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4054 4055 return calendar
Creates bond payments calendar as pandas dataframe, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: pandas dataframe object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports pandas dataframe to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
pandas dataframe with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4057 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4058 """ 4059 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4060 Also, creates Markdown file with calendar data, `calendar.md` by default. 4061 4062 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4063 4064 :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains 4065 extended information about bonds: main info, current prices, bond payment calendar, 4066 coupon yields, current yields and some statistics etc. 4067 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4068 :param show: if `True` then also printing bonds payment calendar to the console, 4069 otherwise save to file `calendarFile` only. `False` by default. 4070 :return: multilines text in Markdown format with bonds payment calendar as a table. 4071 """ 4072 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4073 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4074 4075 infoText = "# Bond payments calendar\n\n" 4076 4077 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate pandas dataframe with full calendar data 4078 4079 if not calendar.empty: 4080 splitLine = "| | | | | | | | | |\n" 4081 4082 info = [ 4083 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4084 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4085 ] 4086 4087 newMonth = False 4088 notOneBond = calendar["figi"].nunique() > 1 4089 for i, bond in enumerate(calendar.iterrows()): 4090 if newMonth and notOneBond: 4091 info.append(splitLine) 4092 4093 info.append( 4094 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4095 " √" if bond[1]["paid"] else " —", 4096 bond[1]["couponDate"].split("T")[0], 4097 bond[1]["figi"], 4098 bond[1]["ticker"], 4099 bond[1]["couponNumber"], 4100 "{} {}".format( 4101 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4102 bond[1]["payCurrency"], 4103 ), 4104 bond[1]["couponType"], 4105 bond[1]["couponPeriod"], 4106 bond[1]["fixDate"].split("T")[0], 4107 ) 4108 ) 4109 4110 if i < len(calendar.values) - 1: 4111 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4112 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4113 newMonth = False if curDate.month == nextDate.month else True 4114 4115 else: 4116 newMonth = False 4117 4118 infoText += "".join(info) 4119 4120 if show: 4121 uLogger.info("{}".format(infoText)) 4122 4123 if self.calendarFile is not None: 4124 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4125 fH.write(infoText) 4126 4127 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4128 4129 else: 4130 infoText += "No data\n" 4131 4132 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: pandas dataframe object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4134 def OverviewAccounts(self, show: bool = False) -> dict: 4135 """ 4136 Method for parsing and show simple table with all available user accounts. 4137 4138 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4139 4140 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4141 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4142 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4143 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4144 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4145 "closed": "—", "access": "Full access" }, ...}}` 4146 """ 4147 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4148 4149 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4150 accounts = { 4151 item["id"]: { 4152 "type": TKS_ACCOUNT_TYPES[item["type"]], 4153 "name": item["name"], 4154 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4155 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4156 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4157 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4158 } for item in rawAccounts["accounts"] 4159 } 4160 4161 # Raw and parsed data with some fields replaced in "stat" section: 4162 view = { 4163 "rawAccounts": rawAccounts, 4164 "stat": accounts, 4165 } 4166 4167 # --- Prepare simple text table with only accounts data in human-readable format: 4168 if show: 4169 info = [ 4170 "# User accounts\n\n", 4171 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4172 "| Account ID | Type | Status | Name |\n", 4173 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4174 ] 4175 4176 for account in view["stat"].keys(): 4177 info.extend([ 4178 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4179 account, 4180 view["stat"][account]["type"], 4181 view["stat"][account]["status"], 4182 view["stat"][account]["name"], 4183 ) 4184 ]) 4185 4186 infoText = "".join(info) 4187 4188 uLogger.info(infoText) 4189 4190 if self.userAccountsFile: 4191 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4192 fH.write(infoText) 4193 4194 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4195 4196 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4198 def OverviewUserInfo(self, show: bool = False) -> dict: 4199 """ 4200 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4201 4202 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4203 4204 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4205 :return: dict with raw parsed data from server and some calculated statistics about it. 4206 """ 4207 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4208 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4209 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4210 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4211 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4212 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4213 4214 # This is dict with parsed common user data: 4215 userInfo = { 4216 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4217 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4218 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4219 "tariff": rawUserInfo["tariff"], 4220 } 4221 4222 # This is an array of dict with parsed margin statuses for every account IDs: 4223 margins = {} 4224 for accountId in accounts.keys(): 4225 if rawMargins[accountId]: 4226 margins[accountId] = { 4227 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4228 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4229 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4230 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4231 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4232 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4233 } 4234 4235 else: 4236 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4237 4238 unary = {} # unary-connection limits 4239 for item in rawTariffLimits["unaryLimits"]: 4240 if item["limitPerMinute"] in unary.keys(): 4241 unary[item["limitPerMinute"]].extend(item["methods"]) 4242 4243 else: 4244 unary[item["limitPerMinute"]] = item["methods"] 4245 4246 stream = {} # stream-connection limits 4247 for item in rawTariffLimits["streamLimits"]: 4248 if item["limit"] in stream.keys(): 4249 stream[item["limit"]].extend(item["streams"]) 4250 4251 else: 4252 stream[item["limit"]] = item["streams"] 4253 4254 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4255 limits = { 4256 "unary": unary, 4257 "stream": stream, 4258 } 4259 4260 # Raw and parsed data as an output result: 4261 view = { 4262 "rawUserInfo": rawUserInfo, 4263 "rawAccounts": rawAccounts, 4264 "rawMargins": rawMargins, 4265 "rawTariffLimits": rawTariffLimits, 4266 "stat": { 4267 "userInfo": userInfo, 4268 "accounts": accounts, 4269 "margins": margins, 4270 "limits": limits, 4271 }, 4272 } 4273 4274 # --- Prepare text table with user information in human-readable format: 4275 if show: 4276 info = [ 4277 "# Full user information\n\n", 4278 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4279 "## Common information\n\n", 4280 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4281 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4282 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4283 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4284 "\n## User accounts\n\n", 4285 ] 4286 4287 for account in view["stat"]["accounts"].keys(): 4288 info.extend([ 4289 "### ID: [{}]\n\n".format(account), 4290 "| Parameters | Values |\n", 4291 "|----------------------|--------------------------------------------------------------|\n", 4292 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4293 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4294 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4295 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4296 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4297 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4298 ]) 4299 4300 if margins[account]: 4301 info.extend([ 4302 "| Margin status: | Enabled |\n", 4303 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4304 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4305 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4306 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4307 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4308 ]) 4309 4310 else: 4311 info.append("| Margin status: | Disabled |\n\n") 4312 4313 info.extend([ 4314 "\n## Current user tariff limits\n", 4315 "\nSee also:\n", 4316 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4317 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4318 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4319 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4320 "\n### Unary limits\n", 4321 ]) 4322 4323 if unary: 4324 for key, values in sorted(unary.items()): 4325 info.append("\n* Max requests per minute: {}\n".format(key)) 4326 4327 for value in values: 4328 info.append(" - {}\n".format(value)) 4329 4330 else: 4331 info.append("\nNot available\n") 4332 4333 info.append("\n### Stream limits\n") 4334 4335 if stream: 4336 for key, values in sorted(stream.items()): 4337 info.append("\n* Max stream connections: {}\n".format(key)) 4338 4339 for value in values: 4340 info.append(" - {}\n".format(value)) 4341 4342 else: 4343 info.append("\nNot available\n") 4344 4345 infoText = "".join(info) 4346 4347 uLogger.info(infoText) 4348 4349 if self.userInfoFile: 4350 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4351 fH.write(infoText) 4352 4353 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4354 4355 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4358class Args: 4359 """ 4360 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4361 """ 4362 def __init__(self, **kwargs): 4363 self.__dict__.update(kwargs) 4364 4365 def __getattr__(self, item): 4366 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4369def ParseArgs(): 4370 """ 4371 Function get and parse command line keys. 4372 4373 See examples: 4374 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4375 - in russian: https://tim55667757.github.io/TKSBrokerAPI/ 4376 """ 4377 parser = ArgumentParser() # command-line string parser 4378 4379 parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples" 4380 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4381 4382 # --- options: 4383 4384 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.") 4385 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4386 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4387 4388 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4389 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4390 4391 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4392 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4393 4394 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4395 4396 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4397 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4398 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4399 4400 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4401 4402 # --- commands: 4403 4404 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4405 4406 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4407 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4408 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4409 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4410 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4411 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4412 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4413 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4414 4415 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4416 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4417 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4418 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4419 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4420 4421 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4422 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4423 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4424 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4425 4426 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4427 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4428 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4429 4430 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4431 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4432 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4433 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4434 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4435 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4436 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4437 4438 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4439 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4440 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4441 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4442 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4443 4444 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4445 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4446 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4447 4448 cmdArgs = parser.parse_args() 4449 return cmdArgs
Function get and parse command line keys.
See examples:
4452def Main(**kwargs): 4453 """ 4454 Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command. 4455 4456 See examples: 4457 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4458 - in russian: https://tim55667757.github.io/TKSBrokerAPI/ 4459 """ 4460 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4461 4462 if args.debug_level: 4463 uLogger.level = 10 # always debug level by default 4464 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4465 4466 exitCode = 0 4467 start = datetime.now(tzutc()) 4468 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4469 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4470 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4471 )) 4472 4473 # trying to calculate full current version: 4474 buildVersion = __version__ 4475 try: 4476 v = version("tksbrokerapi") 4477 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4478 4479 except Exception: 4480 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4481 4482 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4483 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4484 4485 try: 4486 if args.version: 4487 print("TKSBrokerAPI {}".format(buildVersion)) 4488 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4489 4490 else: 4491 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4492 server = TinkoffBrokerServer( 4493 token=args.token, 4494 accountId=args.account_id, 4495 useCache=not args.no_cache, 4496 ) 4497 4498 # --- set some options: 4499 4500 if args.ticker: 4501 if args.ticker in server.aliasesKeys: 4502 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4503 4504 else: 4505 server.ticker = args.ticker 4506 4507 if args.figi: 4508 server.figi = args.figi 4509 4510 if args.depth is not None: 4511 server.depth = args.depth 4512 4513 # --- do one of commands: 4514 4515 if args.list: 4516 if args.output is not None: 4517 server.instrumentsFile = args.output 4518 4519 server.ShowInstrumentsInfo(show=True) 4520 4521 elif args.list_xlsx: 4522 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4523 4524 elif args.bonds_xlsx is not None: 4525 if args.output is not None: 4526 server.bondsXLSXFile = args.output 4527 4528 if len(args.bonds_xlsx) == 0: 4529 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4530 4531 else: 4532 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4533 4534 elif args.search: 4535 if args.output is not None: 4536 server.searchResultsFile = args.output 4537 4538 server.SearchInstruments(pattern=args.search[0], show=True) 4539 4540 elif args.info: 4541 if not (args.ticker or args.figi): 4542 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4543 raise Exception("Ticker or FIGI required") 4544 4545 if args.output is not None: 4546 server.infoFile = args.output 4547 4548 if args.ticker: 4549 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4550 4551 else: 4552 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4553 4554 elif args.calendar is not None: 4555 if args.output is not None: 4556 server.calendarFile = args.output 4557 4558 if len(args.calendar) == 0: 4559 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4560 4561 else: 4562 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4563 4564 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4565 4566 elif args.price: 4567 if not (args.ticker or args.figi): 4568 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4569 raise Exception("Ticker or FIGI required") 4570 4571 server.GetCurrentPrices(show=True) 4572 4573 elif args.prices is not None: 4574 if args.output is not None: 4575 server.pricesFile = args.output 4576 4577 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4578 4579 elif args.overview: 4580 if args.output is not None: 4581 server.overviewFile = args.output 4582 4583 server.Overview(show=True, details="full") 4584 4585 elif args.overview_digest: 4586 if args.output is not None: 4587 server.overviewDigestFile = args.output 4588 4589 server.Overview(show=True, details="digest") 4590 4591 elif args.overview_positions: 4592 if args.output is not None: 4593 server.overviewPositionsFile = args.output 4594 4595 server.Overview(show=True, details="positions") 4596 4597 elif args.overview_orders: 4598 if args.output is not None: 4599 server.overviewOrdersFile = args.output 4600 4601 server.Overview(show=True, details="orders") 4602 4603 elif args.overview_analytics: 4604 if args.output is not None: 4605 server.overviewAnalyticsFile = args.output 4606 4607 server.Overview(show=True, details="analytics") 4608 4609 elif args.deals is not None: 4610 if args.output is not None: 4611 server.reportFile = args.output 4612 4613 if 0 <= len(args.deals) < 3: 4614 server.Deals( 4615 start=args.deals[0] if len(args.deals) >= 1 else None, 4616 end=args.deals[1] if len(args.deals) == 2 else None, 4617 show=True, # Always show deals report in console 4618 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4619 ) 4620 4621 else: 4622 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4623 raise Exception("Incorrect value") 4624 4625 elif args.history is not None: 4626 if args.output is not None: 4627 server.historyFile = args.output 4628 4629 if 0 <= len(args.history) < 3: 4630 dataReceived = server.History( 4631 start=args.history[0] if len(args.history) >= 1 else None, 4632 end=args.history[1] if len(args.history) == 2 else None, 4633 interval="hour" if args.interval is None or not args.interval else args.interval, 4634 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4635 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4636 show=True, # shows all downloaded candles in console 4637 ) 4638 4639 if args.render_chart is not None and dataReceived is not None: 4640 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4641 4642 server.ShowHistoryChart( 4643 candles=dataReceived, 4644 interact=iChart, 4645 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4646 ) 4647 4648 else: 4649 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4650 raise Exception("Incorrect value") 4651 4652 elif args.load_history is not None: 4653 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4654 4655 if args.render_chart is not None and histData is not None: 4656 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4657 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4658 4659 server.ShowHistoryChart( 4660 candles=histData, 4661 interact=iChart, 4662 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4663 ) 4664 4665 elif args.trade is not None: 4666 if 1 <= len(args.trade) <= 5: 4667 server.Trade( 4668 operation=args.trade[0], 4669 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4670 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4671 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4672 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4673 ) 4674 4675 else: 4676 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4677 4678 elif args.buy is not None: 4679 if 0 <= len(args.buy) <= 4: 4680 server.Buy( 4681 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4682 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4683 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4684 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4685 ) 4686 4687 else: 4688 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4689 4690 elif args.sell is not None: 4691 if 0 <= len(args.sell) <= 4: 4692 server.Sell( 4693 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4694 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4695 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4696 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4697 ) 4698 4699 else: 4700 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4701 4702 elif args.order: 4703 if 4 <= len(args.order) <= 7: 4704 server.Order( 4705 operation=args.order[0], 4706 orderType=args.order[1], 4707 lots=int(args.order[2]), 4708 targetPrice=float(args.order[3]), 4709 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4710 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4711 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4712 ) 4713 4714 else: 4715 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4716 4717 elif args.buy_limit: 4718 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4719 4720 elif args.sell_limit: 4721 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4722 4723 elif args.buy_stop: 4724 if 2 <= len(args.buy_stop) <= 7: 4725 server.BuyStop( 4726 lots=int(args.buy_stop[0]), 4727 targetPrice=float(args.buy_stop[1]), 4728 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4729 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4730 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4731 ) 4732 4733 else: 4734 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4735 4736 elif args.sell_stop: 4737 if 2 <= len(args.sell_stop) <= 7: 4738 server.SellStop( 4739 lots=int(args.sell_stop[0]), 4740 targetPrice=float(args.sell_stop[1]), 4741 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4742 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4743 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4744 ) 4745 4746 else: 4747 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4748 4749 # elif args.buy_order_grid is not None: 4750 # # update order grid work with api v2 4751 # if len(args.buy_order_grid) == 2: 4752 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4753 # 4754 # for order in orderParams: 4755 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4756 # 4757 # else: 4758 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4759 # 4760 # elif args.sell_order_grid is not None: 4761 # # update order grid work with api v2 4762 # if len(args.sell_order_grid) >= 2: 4763 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4764 # 4765 # for order in orderParams: 4766 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4767 # 4768 # else: 4769 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4770 4771 elif args.close_order is not None: 4772 server.CloseOrders(args.close_order) # close only one order 4773 4774 elif args.close_orders is not None: 4775 server.CloseOrders(args.close_orders) # close list of orders 4776 4777 elif args.close_trade: 4778 if not args.ticker: 4779 uLogger.error("`--ticker` key is required for this operation!") 4780 raise Exception("Ticker required") 4781 4782 server.CloseTrades([args.ticker]) # close only one trade 4783 4784 elif args.close_trades is not None: 4785 server.CloseTrades(args.close_trades) # close trades for list of tickers 4786 4787 elif args.close_all is not None: 4788 server.CloseAll(*args.close_all) 4789 4790 elif args.limits: 4791 if args.output is not None: 4792 server.withdrawalLimitsFile = args.output 4793 4794 server.OverviewLimits(show=True) 4795 4796 elif args.user_info: 4797 if args.output is not None: 4798 server.userInfoFile = args.output 4799 4800 server.OverviewUserInfo(show=True) 4801 4802 elif args.account: 4803 if args.output is not None: 4804 server.userAccountsFile = args.output 4805 4806 server.OverviewAccounts(show=True) 4807 4808 else: 4809 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4810 raise Exception("There is no command to execute") 4811 4812 except Exception: 4813 trace = tb.format_exc() 4814 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4815 if e in trace: 4816 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4817 break 4818 4819 uLogger.debug(trace) 4820 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4821 exitCode = 255 # an error occurred, must be open a ticket for this issue 4822 4823 finally: 4824 finish = datetime.now(tzutc()) 4825 4826 if exitCode == 0: 4827 uLogger.debug("All operations were finished success (summary code is 0).") 4828 4829 else: 4830 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4831 os.path.abspath(uLog.defaultLogFile), exitCode, 4832 )) 4833 4834 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4835 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4836 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4837 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4838 )) 4839 4840 if not kwargs: 4841 sys.exit(exitCode) 4842 4843 else: 4844 return exitCode
Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.
See examples: